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 = client.get_balance_for_btc().await.map_err(|err| {
419 AdminGatewayError::Unexpected(anyhow!(
420 "Balance not available: {}",
421 err.fmt_compact_anyhow()
422 ))
423 })?;
424
425 wallet_module
430 .max_sendable_amount(balance, fee)
431 .await
432 .map_err(|err| AdminGatewayError::WithdrawError {
433 failure_reason: format!(
434 "Insufficient funds. Balance: {balance} Fee: {fee}: {}",
435 err.fmt_compact_anyhow()
436 ),
437 })?
438 }
439 BitcoinAmountOrAll::Amount(a) => a,
440 };
441
442 let operation_id = wallet_module
443 .send(
444 address.as_unchecked().clone(),
445 withdraw_amount,
446 Some(fee),
447 serde_json::Value::Null,
448 )
449 .await
450 .map_err(|e| AdminGatewayError::WithdrawError {
451 failure_reason: e.to_string(),
452 })?;
453
454 let result = wallet_module
455 .await_final_send_operation_state(operation_id)
456 .await
457 .map_err(|e| AdminGatewayError::WithdrawError {
458 failure_reason: e.to_string(),
459 })?;
460
461 let fees = PegOutFees::from_amount(fee);
462
463 match result {
464 fedimint_walletv2_client::FinalSendOperationState::Success(txid) => {
465 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds via walletv2");
466 Ok(WithdrawResponse { txid, fees })
467 }
468 fedimint_walletv2_client::FinalSendOperationState::Aborted => {
469 Err(AdminGatewayError::WithdrawError {
470 failure_reason: "Withdrawal transaction was aborted".to_string(),
471 })
472 }
473 fedimint_walletv2_client::FinalSendOperationState::Failure => {
474 Err(AdminGatewayError::WithdrawError {
475 failure_reason: "Withdrawal failed".to_string(),
476 })
477 }
478 }
479}
480
481async fn calculate_max_withdrawable(
483 client: &ClientHandleArc,
484 address: &Address,
485) -> AdminResult<WithdrawDetails> {
486 let balance = client.get_balance_for_btc().await.map_err(|err| {
487 AdminGatewayError::Unexpected(anyhow!(
488 "Balance not available: {}",
489 err.fmt_compact_anyhow()
490 ))
491 })?;
492
493 if let Ok(wallet_module) =
494 client.get_first_module::<fedimint_walletv2_client::WalletClientModule>()
495 {
496 let fee = wallet_module
497 .send_fee()
498 .await
499 .map_err(|e| AdminGatewayError::WithdrawError {
500 failure_reason: e.to_string(),
501 })?;
502
503 let max_withdrawable = wallet_module
504 .max_sendable_amount(balance, fee)
505 .await
506 .map_err(|err| AdminGatewayError::WithdrawError {
507 failure_reason: err.fmt_compact_anyhow().to_string(),
508 })?;
509
510 let federation_fees = balance
515 .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
516 .saturating_sub(Amount::from_sats(fee.to_sat()));
517
518 return Ok(WithdrawDetails {
519 amount: Amount::from_sats(max_withdrawable.to_sat()),
520 mint_fees: Some(federation_fees),
521 peg_out_fees: PegOutFees::from_amount(fee),
522 });
523 }
524
525 let Ok(wallet_module) = client.get_first_module::<WalletClientModule>() else {
526 return Err(AdminGatewayError::Unexpected(anyhow!(
527 "No wallet module found"
528 )));
529 };
530
531 let (max_withdrawable, peg_out_fees) = wallet_module
532 .max_withdrawable_amount(address, balance)
533 .await
534 .map_err(|err| AdminGatewayError::WithdrawError {
535 failure_reason: err.fmt_compact_anyhow().to_string(),
536 })?;
537
538 let federation_fees = balance
543 .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
544 .saturating_sub(Amount::from_sats(peg_out_fees.amount().to_sat()));
545
546 Ok(WithdrawDetails {
547 amount: Amount::from_sats(max_withdrawable.to_sat()),
548 mint_fees: Some(federation_fees),
549 peg_out_fees,
550 })
551}
552
553impl Gateway {
554 fn get_bitcoind_client(
557 opts: &GatewayOpts,
558 network: bitcoin::Network,
559 gateway_id: &PublicKey,
560 ) -> anyhow::Result<(BitcoindClient, ChainSource)> {
561 let bitcoind_username = opts
562 .bitcoind_username
563 .clone()
564 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
565 let url = opts.bitcoind_url.clone().expect("No bitcoind url set");
566 let password = opts
567 .bitcoind_password
568 .clone()
569 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
570
571 let chain_source = ChainSource::Bitcoind {
572 username: bitcoind_username.clone(),
573 password: password.clone(),
574 server_url: url.clone(),
575 };
576 let wallet_name = format!("gatewayd-{gateway_id}");
577 let client = BitcoindClient::new(&url, bitcoind_username, password, &wallet_name, network)?;
578 Ok((client, chain_source))
579 }
580
581 pub async fn new_with_default_modules(
584 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
585 ) -> anyhow::Result<Gateway> {
586 let opts = GatewayOpts::parse();
587 let gateway_parameters = opts.to_gateway_parameters()?;
588 let decoders = ModuleDecoderRegistry::default();
589
590 let db_path = opts.data_dir.join(DB_FILE);
591 let gateway_db = match opts.db_backend {
592 DatabaseBackend::RocksDb => {
593 debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
594 Database::new(
595 fedimint_rocksdb::RocksDb::build(db_path).open().await?,
596 decoders,
597 )
598 }
599 DatabaseBackend::CursedRedb => {
600 debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
601 Database::new(
602 fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
603 decoders,
604 )
605 }
606 };
607
608 apply_migrations(
611 &gateway_db,
612 (),
613 "gatewayd".to_string(),
614 get_gatewayd_database_migrations(),
615 None,
616 None,
617 )
618 .await?;
619
620 let http_id = Self::load_or_create_gateway_keypair(&gateway_db, RegisteredProtocol::Http)
623 .await
624 .public_key();
625 let (dyn_bitcoin_rpc, chain_source) =
626 match (opts.bitcoind_url.as_ref(), opts.esplora_url.as_ref()) {
627 (Some(_), None) => {
628 let (client, chain_source) =
629 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
630 (client.into_dyn(), chain_source)
631 }
632 (None, Some(url)) => {
633 let client = EsploraClient::new(url)
634 .expect("Could not create EsploraClient")
635 .into_dyn();
636 let chain_source = ChainSource::Esplora {
637 server_url: url.clone(),
638 };
639 (client, chain_source)
640 }
641 (Some(_), Some(_)) => {
642 let (client, chain_source) =
644 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
645 (client.into_dyn(), chain_source)
646 }
647 _ => unreachable!("ArgGroup already enforced XOR relation"),
648 };
649
650 let mut registry = ClientModuleInitRegistry::new();
653 registry.attach(MintClientInit);
654 registry.attach(MintV2ClientInit);
655 registry.attach(WalletClientInit::new(dyn_bitcoin_rpc));
656 registry.attach(fedimint_walletv2_client::WalletClientInit);
657
658 let client_builder =
659 GatewayClientBuilder::new(opts.data_dir.clone(), registry, opts.db_backend).await?;
660
661 let gateway_state = if Self::load_mnemonic(&gateway_db).await.is_some() {
662 GatewayState::Disconnected
663 } else {
664 if gateway_parameters.skip_setup {
667 let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
668 info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
669 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
670 |e| {
671 AdminGatewayError::MnemonicError(anyhow!(format!(
672 "Seed phrase provided in environment was invalid {e:?}"
673 )))
674 },
675 )?
676 } else {
677 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
678 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
679 };
680
681 Client::store_encodable_client_secret(&gateway_db, mnemonic.to_entropy())
682 .await
683 .map_err(AdminGatewayError::MnemonicError)?;
684 GatewayState::Disconnected
685 } else {
686 GatewayState::NotConfigured { mnemonic_sender }
687 }
688 };
689
690 info!(
691 target: LOG_GATEWAY,
692 version = %fedimint_build_code_version_env!(),
693 "Starting gatewayd",
694 );
695
696 Gateway::new(
697 opts.mode,
698 gateway_parameters,
699 gateway_db,
700 client_builder,
701 gateway_state,
702 chain_source,
703 )
704 .await
705 }
706
707 async fn new(
710 lightning_mode: LightningMode,
711 gateway_parameters: GatewayParameters,
712 gateway_db: Database,
713 client_builder: GatewayClientBuilder,
714 gateway_state: GatewayState,
715 chain_source: ChainSource,
716 ) -> anyhow::Result<Gateway> {
717 let num_route_hints = gateway_parameters.num_route_hints;
718 let network = gateway_parameters.network;
719
720 let task_group = TaskGroup::new();
721 task_group.install_kill_handler();
722
723 let mut registrations = BTreeMap::new();
724 if let Some(http_url) = gateway_parameters.versioned_api {
725 registrations.insert(
726 RegisteredProtocol::Http,
727 Registration::new(&gateway_db, http_url, RegisteredProtocol::Http).await,
728 );
729 }
730
731 let iroh_sk = Self::load_or_create_iroh_key(&gateway_db).await;
732 if gateway_parameters.iroh_listen.is_some() {
733 let endpoint_url = SafeUrl::parse(&format!("iroh://{}", iroh_sk.public()))?;
734 registrations.insert(
735 RegisteredProtocol::Iroh,
736 Registration::new(&gateway_db, endpoint_url, RegisteredProtocol::Iroh).await,
737 );
738 }
739
740 Ok(Self {
741 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
742 lightning_mode,
743 state: Arc::new(RwLock::new(gateway_state)),
744 client_builder,
745 gateway_db: gateway_db.clone(),
746 listen: gateway_parameters.listen,
747 metrics_listen: gateway_parameters.metrics_listen,
748 task_group,
749 bcrypt_password_hash: gateway_parameters.bcrypt_password_hash.to_string(),
750 bcrypt_liquidity_manager_password_hash: gateway_parameters
751 .bcrypt_liquidity_manager_password_hash
752 .map(|h| h.to_string()),
753 num_route_hints,
754 network,
755 chain_source,
756 default_routing_fees: gateway_parameters.default_routing_fees,
757 default_transaction_fees: gateway_parameters.default_transaction_fees,
758 iroh_sk,
759 iroh_dns: gateway_parameters.iroh_dns,
760 iroh_relays: gateway_parameters.iroh_relays,
761 iroh_listen: gateway_parameters.iroh_listen,
762 registrations,
763 })
764 }
765
766 async fn load_or_create_gateway_keypair(
767 gateway_db: &Database,
768 protocol: RegisteredProtocol,
769 ) -> secp256k1::Keypair {
770 let mut dbtx = gateway_db.begin_transaction().await;
771 let keypair = dbtx.load_or_create_gateway_keypair(protocol).await;
772 dbtx.commit_tx().await;
773 keypair
774 }
775
776 async fn load_or_create_iroh_key(gateway_db: &Database) -> iroh::SecretKey {
779 let mut dbtx = gateway_db.begin_transaction().await;
780 let iroh_sk = dbtx.load_or_create_iroh_key().await;
781 dbtx.commit_tx().await;
782 iroh_sk
783 }
784
785 pub async fn http_gateway_id(&self) -> PublicKey {
786 Self::load_or_create_gateway_keypair(&self.gateway_db, RegisteredProtocol::Http)
787 .await
788 .public_key()
789 }
790
791 async fn get_state(&self) -> GatewayState {
792 self.state.read().await.clone()
793 }
794
795 pub async fn dump_database(
798 dbtx: &mut DatabaseTransaction<'_>,
799 prefix_names: Vec<String>,
800 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
801 dbtx.dump_database(prefix_names).await
802 }
803
804 pub async fn run(
809 self,
810 runtime: Arc<tokio::runtime::Runtime>,
811 mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
812 ) -> anyhow::Result<TaskShutdownToken> {
813 install_crypto_provider().await;
814 self.register_clients_timer();
815 self.load_clients().await?;
816 self.start_gateway(runtime, mnemonic_receiver.resubscribe());
817 self.spawn_backup_task();
818 fedimint_metrics::spawn_api_server(self.metrics_listen, self.task_group.clone()).await?;
820 let handle = self.task_group.make_handle();
822 run_webserver(Arc::new(self), mnemonic_receiver.resubscribe()).await?;
823 let shutdown_receiver = handle.make_shutdown_rx();
824 Ok(shutdown_receiver)
825 }
826
827 fn spawn_backup_task(&self) {
830 let self_copy = self.clone();
831 self.task_group
832 .spawn_cancellable_silent("backup ecash", async move {
833 const BACKUP_UPDATE_INTERVAL: Duration = Duration::from_hours(1);
834 let mut interval = tokio::time::interval(BACKUP_UPDATE_INTERVAL);
835 interval.tick().await;
836 loop {
837 {
838 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
839 self_copy.backup_all_federations(&mut dbtx).await;
840 dbtx.commit_tx().await;
841 interval.tick().await;
842 }
843 }
844 });
845 }
846
847 pub async fn backup_all_federations(&self, dbtx: &mut DatabaseTransaction<'_, Committable>) {
851 const BACKUP_THRESHOLD_DURATION: Duration = Duration::from_hours(24);
854
855 let now = fedimint_core::time::now();
856 let threshold = now
857 .checked_sub(BACKUP_THRESHOLD_DURATION)
858 .expect("Cannot be negative");
859 for (id, last_backup) in dbtx.load_backup_records().await {
860 match last_backup {
861 Some(backup_time) if backup_time < threshold => {
862 let fed_manager = self.federation_manager.read().await;
863 fed_manager.backup_federation(&id, dbtx, now).await;
864 }
865 None => {
866 let fed_manager = self.federation_manager.read().await;
867 fed_manager.backup_federation(&id, dbtx, now).await;
868 }
869 _ => {}
870 }
871 }
872 }
873
874 fn start_gateway(
877 &self,
878 runtime: Arc<tokio::runtime::Runtime>,
879 mut mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
880 ) {
881 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 60;
882
883 let self_copy = self.clone();
884 let tg = self.task_group.clone();
885 self.task_group.spawn(
886 "Subscribe to intercepted lightning payments in stream",
887 |handle| async move {
888 loop {
890 if handle.is_shutting_down() {
891 info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
892 break;
893 }
894
895 if let GatewayState::NotConfigured{ .. } = self_copy.get_state().await {
896 info!(
897 target: LOG_GATEWAY,
898 "Waiting for the mnemonic to be set before starting lightning receive loop."
899 );
900 info!(
901 target: LOG_GATEWAY,
902 "You might need to provide it from the UI or refer to documentation w.r.t how to initialize it."
903 );
904
905 let _ = mnemonic_receiver.recv().await;
906 info!(
907 target: LOG_GATEWAY,
908 "Received mnemonic, attempting to start lightning receive loop"
909 );
910 }
911
912 let payment_stream_task_group = tg.make_subgroup();
913 let lnrpc_route = self_copy.create_lightning_client(runtime.clone()).await;
914
915 debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
916 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
917 {
918 Ok((stream, ln_client)) => (stream, ln_client),
919 Err(err) => {
920 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
921 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
929 crit!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Lightning payment stream task group shutdown");
930 }
931 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
932 continue
933 }
934 };
935
936 self_copy.set_gateway_state(GatewayState::Connected).await;
938 info!(target: LOG_GATEWAY, "Established lightning payment stream");
939
940 let route_payments_response =
941 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
942
943 self_copy.set_gateway_state(GatewayState::Disconnected).await;
944 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
945 crit!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Lightning payment stream task group shutdown");
946 }
947
948 self_copy.unannounce_from_all_federations().await;
949
950 match route_payments_response {
951 ReceivePaymentStreamAction::RetryAfterDelay => {
952 warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
953 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
954 }
955 ReceivePaymentStreamAction::NoRetry => break,
956 }
957 }
958 },
959 );
960 }
961
962 async fn route_lightning_payments<'a>(
966 &'a self,
967 handle: &TaskHandle,
968 mut stream: RouteHtlcStream<'a>,
969 ln_client: Arc<dyn ILnRpcClient>,
970 ) -> ReceivePaymentStreamAction {
971 let LightningInfo::Connected {
972 public_key: lightning_public_key,
973 alias: lightning_alias,
974 network: lightning_network,
975 block_height: _,
976 synced_to_chain,
977 } = ln_client.parsed_node_info().await
978 else {
979 warn!(target: LOG_GATEWAY, "Failed to retrieve Lightning info");
980 return ReceivePaymentStreamAction::RetryAfterDelay;
981 };
982
983 assert!(
984 self.network == lightning_network,
985 "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
986 self.network
987 );
988
989 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
990 info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
991 } else {
992 self.set_gateway_state(GatewayState::Syncing).await;
993 info!(target: LOG_GATEWAY, "Waiting for chain sync");
994 if let Err(err) = ln_client.wait_for_chain_sync().await {
995 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
996 return ReceivePaymentStreamAction::RetryAfterDelay;
997 }
998 }
999
1000 let lightning_context = LightningContext {
1001 lnrpc: LnRpcTracked::new(ln_client, "gateway"),
1002 lightning_public_key,
1003 lightning_alias,
1004 lightning_network,
1005 };
1006 if let GatewayState::ShuttingDown { .. } = self
1007 .set_gateway_state(GatewayState::Running { lightning_context })
1008 .await
1009 {
1010 info!(
1011 target: LOG_GATEWAY,
1012 "Reconnected to the lightning node while shutting down, not accepting payments"
1013 );
1014 } else {
1015 info!(target: LOG_GATEWAY, "Gateway is running");
1016 }
1017
1018 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1019 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1022 let all_federations_configs =
1023 dbtx.load_federation_configs().await.into_iter().collect();
1024 self.register_federations(&all_federations_configs, &self.task_group)
1025 .await;
1026 }
1027
1028 let htlc_task_group = self.task_group.make_subgroup();
1031 if handle
1032 .cancel_on_shutdown(async move {
1033 loop {
1034 let payment_request_or = tokio::select! {
1035 payment_request_or = stream.next() => {
1036 payment_request_or
1037 }
1038 () = self.is_shutting_down_safely() => {
1039 break;
1040 }
1041 };
1042
1043 let Some(payment_request) = payment_request_or else {
1044 warn!(
1045 target: LOG_GATEWAY,
1046 "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
1047 );
1048 break;
1049 };
1050
1051 let state_guard = self.state.read().await;
1052 if let GatewayState::Running { ref lightning_context } = *state_guard {
1053 let gateway = self.clone();
1055 let lightning_context = lightning_context.clone();
1056 htlc_task_group.spawn_cancellable_silent(
1057 "handle_lightning_payment",
1058 async move {
1059 let start = fedimint_core::time::now();
1060 let outcome = gateway
1061 .handle_lightning_payment(payment_request, &lightning_context)
1062 .await;
1063 metrics::HTLC_HANDLING_DURATION_SECONDS
1064 .with_label_values(&[outcome])
1065 .observe(
1066 fedimint_core::time::now()
1067 .duration_since(start)
1068 .unwrap_or_default()
1069 .as_secs_f64(),
1070 );
1071 },
1072 );
1073 } else {
1074 warn!(
1075 target: LOG_GATEWAY,
1076 state = %state_guard,
1077 "Gateway isn't in a running state, cannot handle incoming payments."
1078 );
1079 break;
1080 }
1081 }
1082 })
1083 .await
1084 .is_ok()
1085 {
1086 warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
1087 ReceivePaymentStreamAction::RetryAfterDelay
1088 } else {
1089 info!(target: LOG_GATEWAY, "Received shutdown signal");
1090 ReceivePaymentStreamAction::NoRetry
1091 }
1092 }
1093
1094 async fn is_shutting_down_safely(&self) {
1097 loop {
1098 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
1099 return;
1100 }
1101
1102 fedimint_core::task::sleep(Duration::from_secs(1)).await;
1103 }
1104 }
1105
1106 async fn handle_lightning_payment(
1116 &self,
1117 payment_request: InterceptPaymentRequest,
1118 lightning_context: &LightningContext,
1119 ) -> &'static str {
1120 info!(
1121 target: LOG_GATEWAY,
1122 lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
1123 "Intercepting lightning payment",
1124 );
1125
1126 let lnv2_start = fedimint_core::time::now();
1127 let lnv2_result = self
1128 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
1129 .await;
1130 let lnv2_outcome = if lnv2_result.is_ok() {
1131 "success"
1132 } else {
1133 "error"
1134 };
1135 metrics::HTLC_LNV2_ATTEMPT_DURATION_SECONDS
1136 .with_label_values(&[lnv2_outcome])
1137 .observe(
1138 fedimint_core::time::now()
1139 .duration_since(lnv2_start)
1140 .unwrap_or_default()
1141 .as_secs_f64(),
1142 );
1143 if lnv2_result.is_ok() {
1144 return "lnv2";
1145 }
1146
1147 let lnv1_start = fedimint_core::time::now();
1148 let lnv1_result = self
1149 .try_handle_lightning_payment_ln_legacy(&payment_request, lightning_context)
1150 .await;
1151 let lnv1_outcome = if lnv1_result.is_ok() {
1152 "success"
1153 } else {
1154 "error"
1155 };
1156 metrics::HTLC_LNV1_ATTEMPT_DURATION_SECONDS
1157 .with_label_values(&[lnv1_outcome])
1158 .observe(
1159 fedimint_core::time::now()
1160 .duration_since(lnv1_start)
1161 .unwrap_or_default()
1162 .as_secs_f64(),
1163 );
1164 if lnv1_result.is_ok() {
1165 return "lnv1";
1166 }
1167
1168 let is_federation_scid = match payment_request.short_channel_id {
1173 Some(scid) => self
1174 .federation_manager
1175 .read()
1176 .await
1177 .get_client_for_index(scid)
1178 .is_some(),
1179 None => false,
1180 };
1181
1182 if is_federation_scid {
1183 warn!(
1190 target: LOG_GATEWAY,
1191 payment_hash = %payment_request.payment_hash,
1192 short_channel_id = ?payment_request.short_channel_id,
1193 amount_msat = payment_request.amount_msat,
1194 incoming_chan_id = payment_request.incoming_chan_id,
1195 htlc_id = payment_request.htlc_id,
1196 lnv2_err = ?lnv2_result.as_ref().err(),
1197 lnv1_err = ?lnv1_result.as_ref().err(),
1198 "Unmatched lightning payment for federation scid: cancelling HTLC",
1199 );
1200 Self::cancel_unmatched_lightning_payment(payment_request, lightning_context).await;
1201 "cancel"
1202 } else {
1203 Self::forward_lightning_payment(payment_request, lightning_context).await;
1208 "forward"
1209 }
1210 }
1211
1212 async fn try_handle_lightning_payment_lnv2(
1215 &self,
1216 htlc_request: &InterceptPaymentRequest,
1217 lightning_context: &LightningContext,
1218 ) -> Result<()> {
1219 let (contract, client) = self
1225 .get_registered_incoming_contract_and_client_v2(
1226 PaymentImage::Hash(htlc_request.payment_hash),
1227 htlc_request.amount_msat,
1228 )
1229 .await?;
1230
1231 if let Err(err) = client
1232 .get_first_module::<GatewayClientModuleV2>()
1233 .expect("Must have client module")
1234 .relay_incoming_htlc(
1235 htlc_request.payment_hash,
1236 htlc_request.incoming_chan_id,
1237 htlc_request.htlc_id,
1238 contract,
1239 htlc_request.amount_msat,
1240 )
1241 .await
1242 {
1243 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error relaying incoming lightning payment");
1244
1245 let outcome = InterceptPaymentResponse {
1246 action: PaymentAction::Cancel,
1247 payment_hash: htlc_request.payment_hash,
1248 incoming_chan_id: htlc_request.incoming_chan_id,
1249 htlc_id: htlc_request.htlc_id,
1250 };
1251
1252 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1253 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
1254 }
1255 }
1256
1257 Ok(())
1258 }
1259
1260 async fn try_handle_lightning_payment_ln_legacy(
1263 &self,
1264 htlc_request: &InterceptPaymentRequest,
1265 lightning_context: &LightningContext,
1266 ) -> Result<()> {
1267 let Some(federation_index) = htlc_request.short_channel_id else {
1269 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1270 "Incoming payment has not last hop short channel id".to_string(),
1271 )));
1272 };
1273
1274 let Some(client) = self
1275 .federation_manager
1276 .read()
1277 .await
1278 .get_client_for_index(federation_index)
1279 else {
1280 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())));
1281 };
1282
1283 client
1288 .borrow()
1289 .with(|client| async {
1290 let htlc = htlc_request.clone().try_into();
1291 match htlc {
1292 Ok(htlc) => {
1293 let lnv1 =
1294 client
1295 .get_first_module::<GatewayClientModule>()
1296 .map_err(|_| {
1297 PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1298 "Federation does not have LNv1 module".to_string(),
1299 ))
1300 })?;
1301 match lnv1
1302 .gateway_handle_intercepted_htlc(htlc, async {
1303 Ok(lightning_context.lnrpc.info().await?.block_height)
1304 })
1305 .await
1306 {
1307 Ok(_) => Ok(()),
1308 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1309 format!("Error intercepting lightning payment {e:?}"),
1310 ))),
1311 }
1312 }
1313 _ => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1314 "Could not convert InterceptHtlcResult into an HTLC".to_string(),
1315 ))),
1316 }
1317 })
1318 .await
1319 }
1320
1321 async fn cancel_unmatched_lightning_payment(
1335 htlc_request: InterceptPaymentRequest,
1336 lightning_context: &LightningContext,
1337 ) {
1338 let outcome = InterceptPaymentResponse {
1339 action: PaymentAction::Cancel,
1340 payment_hash: htlc_request.payment_hash,
1341 incoming_chan_id: htlc_request.incoming_chan_id,
1342 htlc_id: htlc_request.htlc_id,
1343 };
1344
1345 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1346 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1347 }
1348 }
1349
1350 async fn forward_lightning_payment(
1355 htlc_request: InterceptPaymentRequest,
1356 lightning_context: &LightningContext,
1357 ) {
1358 let outcome = InterceptPaymentResponse {
1359 action: PaymentAction::Forward,
1360 payment_hash: htlc_request.payment_hash,
1361 incoming_chan_id: htlc_request.incoming_chan_id,
1362 htlc_id: htlc_request.htlc_id,
1363 };
1364
1365 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1366 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1367 }
1368 }
1369
1370 async fn set_gateway_state(&self, state: GatewayState) -> GatewayState {
1381 let mut lock = self.state.write().await;
1382
1383 if let GatewayState::ShuttingDown { .. } = *lock {
1384 match state {
1385 GatewayState::Running { lightning_context } => {
1386 *lock = GatewayState::ShuttingDown { lightning_context };
1387 }
1388 ignored => {
1389 info!(
1390 target: LOG_GATEWAY,
1391 ignored_state = %ignored,
1392 "Gateway is shutting down, ignoring state change"
1393 );
1394 }
1395 }
1396 } else {
1397 *lock = state;
1398 }
1399
1400 lock.clone()
1401 }
1402
1403 #[doc(hidden)]
1411 pub async fn set_gateway_state_out_of_band(&self, state: GatewayState) {
1412 self.set_gateway_state(state).await;
1413 }
1414
1415 pub async fn handle_get_federation_config(
1418 &self,
1419 federation_id_or: Option<FederationId>,
1420 ) -> AdminResult<GatewayFedConfig> {
1421 if !matches!(self.get_state().await, GatewayState::Running { .. }) {
1422 return Ok(GatewayFedConfig {
1423 federations: BTreeMap::new(),
1424 });
1425 }
1426
1427 let federations = if let Some(federation_id) = federation_id_or {
1428 let mut federations = BTreeMap::new();
1429 federations.insert(
1430 federation_id,
1431 self.federation_manager
1432 .read()
1433 .await
1434 .get_federation_config(federation_id)
1435 .await?,
1436 );
1437 federations
1438 } else {
1439 self.federation_manager
1440 .read()
1441 .await
1442 .get_all_federation_configs()
1443 .await
1444 };
1445
1446 Ok(GatewayFedConfig { federations })
1447 }
1448
1449 pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
1452 let client = self.select_client(payload.federation_id).await?;
1453
1454 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1455 let address = wallet_module
1456 .allocate_deposit_address_expert_only(())
1457 .await?
1458 .address;
1459 Ok(address)
1460 } else if let Ok(wallet_module) = client
1461 .value()
1462 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1463 {
1464 Ok(wallet_module.receive().await)
1465 } else {
1466 Err(AdminGatewayError::Unexpected(anyhow!(
1467 "No wallet module found"
1468 )))
1469 }
1470 }
1471
1472 async fn handle_pay_invoice_msg(
1475 &self,
1476 payload: fedimint_ln_client::pay::PayInvoicePayload,
1477 ) -> Result<Preimage> {
1478 let GatewayState::Running { .. } = self.get_state().await else {
1479 return Err(PublicGatewayError::Lightning(
1480 LightningRpcError::FailedToConnect,
1481 ));
1482 };
1483
1484 debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1485 let client = self.select_client(payload.federation_id).await?;
1486 let contract_id = payload.contract_id;
1487 let gateway_module = &client
1488 .value()
1489 .get_first_module::<GatewayClientModule>()
1490 .map_err(LNv1Error::OutgoingPayment)
1491 .map_err(PublicGatewayError::LNv1)?;
1492 let operation_id = gateway_module
1493 .gateway_pay_bolt11_invoice(payload)
1494 .await
1495 .map_err(LNv1Error::OutgoingPayment)
1496 .map_err(PublicGatewayError::LNv1)?;
1497 let mut updates = gateway_module
1498 .gateway_subscribe_ln_pay(operation_id)
1499 .await
1500 .map_err(LNv1Error::OutgoingPayment)
1501 .map_err(PublicGatewayError::LNv1)?
1502 .into_stream();
1503 while let Some(update) = updates.next().await {
1504 match update {
1505 GatewayExtPayStates::Success { preimage, .. } => {
1506 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1507 return Ok(preimage);
1508 }
1509 GatewayExtPayStates::Fail {
1510 error,
1511 error_message,
1512 } => {
1513 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1514 error: Box::new(error),
1515 message: format!(
1516 "{error_message} while paying invoice with contract id {contract_id}"
1517 ),
1518 }));
1519 }
1520 GatewayExtPayStates::Canceled { error } => {
1521 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1522 error: Box::new(error.clone()),
1523 message: format!(
1524 "Cancelled with {error} while paying invoice with contract id {contract_id}"
1525 ),
1526 }));
1527 }
1528 GatewayExtPayStates::Created => {
1529 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1530 }
1531 other => {
1532 debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1533 }
1534 }
1535 }
1536
1537 Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1538 anyhow!("Ran out of state updates while paying invoice"),
1539 )))
1540 }
1541
1542 pub async fn handle_backup_msg(
1545 &self,
1546 BackupPayload { federation_id }: BackupPayload,
1547 ) -> AdminResult<()> {
1548 let federation_manager = self.federation_manager.read().await;
1549 let client = federation_manager
1550 .client(&federation_id)
1551 .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1552 format!("Gateway has not connected to {federation_id}")
1553 )))?
1554 .value();
1555 let metadata: BTreeMap<String, String> = BTreeMap::new();
1556 #[allow(deprecated)]
1557 client
1558 .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1559 metadata,
1560 ))
1561 .await?;
1562 Ok(())
1563 }
1564
1565 pub async fn handle_recheck_address_msg(
1567 &self,
1568 payload: DepositAddressRecheckPayload,
1569 ) -> AdminResult<()> {
1570 let client = self.select_client(payload.federation_id).await?;
1571
1572 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1573 wallet_module
1574 .recheck_pegin_address_by_address(payload.address)
1575 .await?;
1576 Ok(())
1577 } else if client
1578 .value()
1579 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1580 .is_ok()
1581 {
1582 Ok(())
1584 } else {
1585 Err(AdminGatewayError::Unexpected(anyhow!(
1586 "No wallet module found"
1587 )))
1588 }
1589 }
1590
1591 pub async fn handle_receive_ecash_msg(
1593 &self,
1594 payload: ReceiveEcashPayload,
1595 ) -> Result<ReceiveEcashResponse> {
1596 let federation_id_prefix = base32::decode_prefixed::<fedimint_mintv2_client::ECash>(
1598 FEDIMINT_PREFIX,
1599 &payload.notes,
1600 )
1601 .ok()
1602 .and_then(|e| e.mint())
1603 .map(|id| id.to_prefix())
1604 .or_else(|| {
1605 OOBNotes::from_str(&payload.notes)
1606 .ok()
1607 .map(|n| n.federation_id_prefix())
1608 })
1609 .ok_or_else(|| PublicGatewayError::ReceiveEcashError {
1610 failure_reason: "Invalid ecash format: could not parse as ECash or OOBNotes"
1611 .to_string(),
1612 })?;
1613
1614 let client = self
1615 .federation_manager
1616 .read()
1617 .await
1618 .get_client_for_federation_id_prefix(federation_id_prefix)
1619 .ok_or(FederationNotConnected {
1620 federation_id_prefix,
1621 })?;
1622
1623 if let Ok(mint) = client.value().get_first_module::<MintClientModule>() {
1625 let notes = OOBNotes::from_str(&payload.notes).map_err(|e| {
1626 PublicGatewayError::ReceiveEcashError {
1627 failure_reason: format!("Expected OOBNotes for MintV1 federation: {e}"),
1628 }
1629 })?;
1630 let amount = notes.total_amount();
1631
1632 let operation_id = mint.reissue_external_notes(notes, ()).await.map_err(|e| {
1633 PublicGatewayError::ReceiveEcashError {
1634 failure_reason: e.to_string(),
1635 }
1636 })?;
1637
1638 let mut updates = mint
1639 .subscribe_reissue_external_notes(operation_id)
1640 .await
1641 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1642 failure_reason: format!("Could not subscribe to reissue operation: {e}"),
1643 })?
1644 .into_stream();
1645
1646 let mut reissued = false;
1651 while let Some(update) = updates.next().await {
1652 match update {
1653 ReissueExternalNotesState::Failed(failure_reason) => {
1654 return Err(PublicGatewayError::ReceiveEcashError { failure_reason });
1655 }
1656 ReissueExternalNotesState::Done => reissued = true,
1657 ReissueExternalNotesState::Created | ReissueExternalNotesState::Issuing => {}
1658 }
1659 }
1660
1661 if !reissued {
1662 return Err(PublicGatewayError::ReceiveEcashError {
1663 failure_reason: "Reissue operation ended before the notes were reissued"
1664 .to_string(),
1665 });
1666 }
1667
1668 Ok(ReceiveEcashResponse { amount })
1669 } else if let Ok(mint) = client.value().get_first_module::<MintV2ClientModule>() {
1670 let ecash: fedimint_mintv2_client::ECash =
1671 base32::decode_prefixed(FEDIMINT_PREFIX, &payload.notes).map_err(|e| {
1672 PublicGatewayError::ReceiveEcashError {
1673 failure_reason: format!("Expected ECash for MintV2 federation: {e}"),
1674 }
1675 })?;
1676 let amount = ecash.amount();
1677
1678 let operation_id = mint
1679 .receive(ecash, serde_json::Value::Null)
1680 .await
1681 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1682 failure_reason: e.to_string(),
1683 })?;
1684
1685 let final_state = mint
1686 .await_final_receive_operation_state(operation_id)
1687 .await
1688 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1689 failure_reason: e.to_string(),
1690 })?;
1691 match final_state {
1692 fedimint_mintv2_client::FinalReceiveOperationState::Success => {}
1693 fedimint_mintv2_client::FinalReceiveOperationState::Rejected => {
1694 return Err(PublicGatewayError::ReceiveEcashError {
1695 failure_reason: "ECash receive was rejected".to_string(),
1696 });
1697 }
1698 }
1699
1700 Ok(ReceiveEcashResponse { amount })
1701 } else {
1702 Err(PublicGatewayError::ReceiveEcashError {
1703 failure_reason: "No mint module found".to_string(),
1704 })
1705 }
1706 }
1707
1708 pub async fn handle_get_invoice_msg(
1711 &self,
1712 payload: GetInvoiceRequest,
1713 ) -> AdminResult<Option<GetInvoiceResponse>> {
1714 let lightning_context = self.get_lightning_context().await?;
1715 let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1716 Ok(invoice)
1717 }
1718
1719 pub async fn handle_withdraw_to_onchain_msg(
1722 &self,
1723 payload: WithdrawToOnchainPayload,
1724 ) -> AdminResult<WithdrawResponse> {
1725 let address = self.handle_get_ln_onchain_address_msg().await?;
1726 let withdraw = WithdrawPayload {
1727 address: address.into_unchecked(),
1728 federation_id: payload.federation_id,
1729 amount: payload.amount,
1730 quoted_fees: None,
1731 };
1732 self.handle_withdraw_msg(withdraw).await
1733 }
1734
1735 pub async fn handle_pegin_from_onchain_msg(
1738 &self,
1739 payload: PeginFromOnchainPayload,
1740 ) -> AdminResult<Txid> {
1741 let deposit = DepositAddressPayload {
1742 federation_id: payload.federation_id,
1743 };
1744 let address = self.handle_address_msg(deposit).await?;
1745 let send_onchain = SendOnchainRequest {
1746 address: address.into_unchecked(),
1747 amount: payload.amount,
1748 fee_rate_sats_per_vbyte: payload.fee_rate_sats_per_vbyte,
1749 };
1750 let txid = self.handle_send_onchain_msg(send_onchain).await?;
1751
1752 Ok(txid)
1753 }
1754
1755 async fn register_federations(
1763 &self,
1764 federations: &BTreeMap<FederationId, FederationConfig>,
1765 register_task_group: &TaskGroup,
1766 ) {
1767 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
1768 info!(
1769 target: LOG_GATEWAY,
1770 "Gateway is shutting down, skipping federation registration"
1771 );
1772 return;
1773 }
1774
1775 if let Ok(lightning_context) = self.get_lightning_context().await {
1776 let route_hints = lightning_context
1777 .lnrpc
1778 .parsed_route_hints(self.num_route_hints)
1779 .await;
1780 if route_hints.is_empty() {
1781 warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
1782 }
1783
1784 for (federation_id, federation_config) in federations {
1785 let routing_fees = match RoutingFees::try_from(federation_config.lightning_fee) {
1790 Ok(routing_fees) => routing_fees,
1791 Err(err) => {
1792 warn!(
1793 target: LOG_GATEWAY,
1794 %federation_id,
1795 err = %err.fmt_compact(),
1796 "Skipping registration, the configured lightning fee cannot be announced. Set a smaller fee with `set_fees`."
1797 );
1798 continue;
1799 }
1800 };
1801
1802 let fed_manager = self.federation_manager.read().await;
1803 if let Some(client) = fed_manager.client(federation_id) {
1804 let client_arc = client.clone().into_value();
1805 let route_hints = route_hints.clone();
1806 let lightning_context = lightning_context.clone();
1807 let registrations =
1808 self.registrations.clone().into_values().collect::<Vec<_>>();
1809
1810 register_task_group.spawn_cancellable_silent(
1811 "register federation",
1812 async move {
1813 let Ok(gateway_client) =
1814 client_arc.get_first_module::<GatewayClientModule>()
1815 else {
1816 return;
1817 };
1818
1819 for registration in registrations {
1820 gateway_client
1821 .try_register_with_federation(
1822 route_hints.clone(),
1823 GW_ANNOUNCEMENT_TTL,
1824 routing_fees,
1825 lightning_context.clone(),
1826 registration.endpoint_url,
1827 registration.keypair,
1828 )
1829 .await;
1830 }
1831 },
1832 );
1833 }
1834 }
1835 }
1836 }
1837
1838 pub async fn select_client(
1841 &self,
1842 federation_id: FederationId,
1843 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
1844 {
1845 self.federation_manager
1846 .read()
1847 .await
1848 .client(&federation_id)
1849 .cloned()
1850 .ok_or(FederationNotConnected {
1851 federation_id_prefix: federation_id.to_prefix(),
1852 })
1853 }
1854
1855 async fn load_mnemonic(gateway_db: &Database) -> Option<Mnemonic> {
1856 let secret = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db)
1857 .await
1858 .ok()?;
1859 Mnemonic::from_entropy(&secret).ok()
1860 }
1861
1862 async fn load_clients(&self) -> AdminResult<()> {
1866 if let GatewayState::NotConfigured { .. } = self.get_state().await {
1867 return Ok(());
1868 }
1869
1870 let mut federation_manager = self.federation_manager.write().await;
1871
1872 let configs = {
1873 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1874 dbtx.load_federation_configs().await
1875 };
1876
1877 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
1878 federation_manager.set_next_index(max_federation_index + 1);
1879 }
1880
1881 let mnemonic = Self::load_mnemonic(&self.gateway_db)
1882 .await
1883 .expect("mnemonic should be set");
1884
1885 for (federation_id, config) in configs {
1886 let federation_index = config.federation_index;
1887 match Box::pin(Spanned::try_new(
1888 info_span!(target: LOG_GATEWAY, "client", federation_id = %federation_id.clone()),
1889 self.client_builder
1890 .build(config, Arc::new(self.clone()), &mnemonic),
1891 ))
1892 .await
1893 {
1894 Ok(client) => {
1895 federation_manager.add_client(federation_index, client);
1896 }
1897 _ => {
1898 warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
1899 }
1900 }
1901 }
1902
1903 Ok(())
1904 }
1905
1906 fn register_clients_timer(&self) {
1912 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1914 info!(target: LOG_GATEWAY, "Spawning register task...");
1915 let gateway = self.clone();
1916 let register_task_group = self.task_group.make_subgroup();
1917 self.task_group.spawn_cancellable("register clients", async move {
1918 loop {
1919 let gateway_state = gateway.get_state().await;
1920 if let GatewayState::Running { .. } = &gateway_state {
1921 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
1922 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
1923 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
1924 } else {
1925 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
1927 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");
1928 sleep(NOT_RUNNING_RETRY).await;
1929 continue;
1930 }
1931
1932 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
1935 }
1936 });
1937 }
1938 }
1939
1940 async fn check_federation_network(
1943 client: &ClientHandleArc,
1944 network: Network,
1945 ) -> AdminResult<()> {
1946 let federation_id = client.federation_id();
1947 let config = client.config().await;
1948
1949 let lnv1_cfg = config
1950 .modules
1951 .values()
1952 .find(|m| LightningCommonInit::KIND == m.kind);
1953
1954 let lnv2_cfg = config
1955 .modules
1956 .values()
1957 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
1958
1959 if lnv1_cfg.is_none() && lnv2_cfg.is_none() {
1961 return Err(AdminGatewayError::ClientCreationError(anyhow!(
1962 "Federation {federation_id} does not have any lightning module (LNv1 or LNv2)"
1963 )));
1964 }
1965
1966 if let Some(cfg) = lnv1_cfg {
1968 let ln_cfg: &LightningClientConfig = cfg.cast()?;
1969
1970 if ln_cfg.network.0 != network {
1971 crit!(
1972 target: LOG_GATEWAY,
1973 federation_id = %federation_id,
1974 network = %network,
1975 "Incorrect LNv1 network for federation",
1976 );
1977 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1978 "Unsupported LNv1 network {}",
1979 ln_cfg.network
1980 ))));
1981 }
1982 }
1983
1984 if let Some(cfg) = lnv2_cfg {
1986 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig = cfg.cast()?;
1987
1988 if ln_cfg.network != network {
1989 crit!(
1990 target: LOG_GATEWAY,
1991 federation_id = %federation_id,
1992 network = %network,
1993 "Incorrect LNv2 network for federation",
1994 );
1995 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1996 "Unsupported LNv2 network {}",
1997 ln_cfg.network
1998 ))));
1999 }
2000 }
2001
2002 Ok(())
2003 }
2004
2005 pub async fn get_lightning_context(
2015 &self,
2016 ) -> std::result::Result<LightningContext, LightningRpcError> {
2017 match self.get_state().await {
2018 GatewayState::Running { lightning_context }
2019 | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
2020 _ => Err(LightningRpcError::FailedToConnect),
2021 }
2022 }
2023
2024 async fn await_lightning_context(&self) -> LightningContext {
2045 loop {
2046 match self.get_lightning_context().await {
2047 Ok(lightning_context) => return lightning_context,
2048 Err(err) => {
2049 let state = self.get_state().await;
2050
2051 warn!(
2052 target: LOG_GATEWAY,
2053 err = %err.fmt_compact(),
2054 %state,
2055 retry_interval_secs = LIGHTNING_CONTEXT_RETRY_INTERVAL.as_secs(),
2056 "Not connected to the lightning node, waiting before asking it again",
2057 );
2058
2059 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
2060 }
2061 }
2062 }
2063 }
2064
2065 pub async fn unannounce_from_all_federations(&self) {
2068 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2069 for registration in self.registrations.values() {
2070 self.federation_manager
2071 .read()
2072 .await
2073 .unannounce_from_all_federations(registration.keypair)
2074 .await;
2075 }
2076 }
2077 }
2078
2079 async fn create_lightning_client(
2080 &self,
2081 runtime: Arc<tokio::runtime::Runtime>,
2082 ) -> Box<dyn ILnRpcClient> {
2083 match self.lightning_mode.clone() {
2084 LightningMode::Lnd {
2085 lnd_rpc_addr,
2086 lnd_tls_cert,
2087 lnd_macaroon,
2088 lnd_time_pref,
2089 lnd_payment_timeout_secs,
2090 } => {
2091 let gateway_db = self.gateway_db.clone();
2096 let lnv2_filter: Lnv2HoldInvoiceFilter = Arc::new(move |hash| {
2097 let gateway_db = gateway_db.clone();
2098 Box::pin(async move {
2099 gateway_db
2100 .begin_transaction_nc()
2101 .await
2102 .load_registered_incoming_contract(PaymentImage::Hash(hash))
2103 .await
2104 .is_some()
2105 })
2106 });
2107
2108 Box::new(GatewayLndClient::new(
2109 lnd_rpc_addr,
2110 lnd_tls_cert,
2111 lnd_macaroon,
2112 lnd_time_pref,
2113 lnd_payment_timeout_secs,
2114 None,
2115 lnv2_filter,
2116 ))
2117 }
2118 LightningMode::Ldk {
2119 lightning_port,
2120 alias,
2121 } => {
2122 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2123 .await
2124 .expect("mnemonic should be set");
2125 retry("create LDK Node", fibonacci_max_one_hour(), || async {
2129 ldk::GatewayLdkClient::new(
2130 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
2131 self.chain_source.clone(),
2132 self.network,
2133 lightning_port,
2134 alias.clone(),
2135 mnemonic.clone(),
2136 runtime.clone(),
2137 )
2138 .map(Box::new)
2139 })
2140 .await
2141 .expect("Could not create LDK Node")
2142 }
2143 }
2144 }
2145}
2146
2147#[async_trait]
2148impl IAdminGateway for Gateway {
2149 type Error = AdminGatewayError;
2150
2151 async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
2154 let GatewayState::Running { lightning_context } = self.get_state().await else {
2155 return Ok(GatewayInfo {
2156 federations: vec![],
2157 federation_fake_scids: None,
2158 version_hash: fedimint_build_code_version_env!().to_string(),
2159 gateway_state: self.state.read().await.to_string(),
2160 lightning_info: LightningInfo::NotConnected,
2161 lightning_mode: self.lightning_mode.clone(),
2162 registrations: self
2163 .registrations
2164 .iter()
2165 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2166 .collect(),
2167 });
2168 };
2169
2170 let dbtx = self.gateway_db.begin_transaction_nc().await;
2171 let federations = self
2172 .federation_manager
2173 .read()
2174 .await
2175 .federation_info_all_federations(dbtx)
2176 .await;
2177
2178 let channels: BTreeMap<u64, FederationId> = federations
2179 .iter()
2180 .map(|federation_info| {
2181 (
2182 federation_info.config.federation_index,
2183 federation_info.federation_id,
2184 )
2185 })
2186 .collect();
2187
2188 let lightning_info = lightning_context.lnrpc.parsed_node_info().await;
2189
2190 Ok(GatewayInfo {
2191 federations,
2192 federation_fake_scids: Some(channels),
2193 version_hash: fedimint_build_code_version_env!().to_string(),
2194 gateway_state: self.state.read().await.to_string(),
2195 lightning_info,
2196 lightning_mode: self.lightning_mode.clone(),
2197 registrations: self
2198 .registrations
2199 .iter()
2200 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2201 .collect(),
2202 })
2203 }
2204
2205 async fn handle_list_channels_msg(
2208 &self,
2209 ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
2210 let context = self.get_lightning_context().await?;
2211 let response = context.lnrpc.list_channels().await?;
2212 Ok(response.channels)
2213 }
2214
2215 async fn handle_payment_summary_msg(
2218 &self,
2219 PaymentSummaryPayload {
2220 start_millis,
2221 end_millis,
2222 }: PaymentSummaryPayload,
2223 ) -> AdminResult<PaymentSummaryResponse> {
2224 let federation_manager = self.federation_manager.read().await;
2225 let fed_configs = federation_manager.get_all_federation_configs().await;
2226 let federation_ids = fed_configs.keys().collect::<Vec<_>>();
2227 let start = UNIX_EPOCH + Duration::from_millis(start_millis);
2228 let end = UNIX_EPOCH + Duration::from_millis(end_millis);
2229
2230 if start > end {
2231 return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
2232 }
2233
2234 let mut outgoing = StructuredPaymentEvents::default();
2235 let mut incoming = StructuredPaymentEvents::default();
2236 for fed_id in federation_ids {
2237 let client = federation_manager
2238 .client(fed_id)
2239 .expect("No client available")
2240 .value();
2241 let all_events = &get_events_for_duration(client, start, end).await;
2242
2243 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
2244 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
2245 outgoing.combine(&mut lnv1_outgoing);
2246 incoming.combine(&mut lnv1_incoming);
2247 outgoing.combine(&mut lnv2_outgoing);
2248 incoming.combine(&mut lnv2_incoming);
2249 }
2250
2251 Ok(PaymentSummaryResponse {
2252 outgoing: PaymentStats::compute(&outgoing),
2253 incoming: PaymentStats::compute(&incoming),
2254 })
2255 }
2256
2257 async fn handle_leave_federation(
2262 &self,
2263 payload: LeaveFedPayload,
2264 ) -> AdminResult<FederationInfo> {
2265 let mut federation_manager = self.federation_manager.write().await;
2268 let mut dbtx = self.gateway_db.begin_transaction().await;
2269
2270 let federation_info = federation_manager
2271 .leave_federation(
2272 payload.federation_id,
2273 &mut dbtx.to_ref_nc(),
2274 self.registrations.values().collect(),
2275 )
2276 .await?;
2277
2278 dbtx.remove_federation_config(payload.federation_id).await;
2279 dbtx.commit_tx().await;
2280 Ok(federation_info)
2281 }
2282
2283 async fn handle_connect_federation(
2288 &self,
2289 payload: ConnectFedPayload,
2290 ) -> AdminResult<FederationInfo> {
2291 let GatewayState::Running { lightning_context } = self.get_state().await else {
2292 return Err(AdminGatewayError::Lightning(
2293 LightningRpcError::FailedToConnect,
2294 ));
2295 };
2296
2297 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
2298 AdminGatewayError::ClientCreationError(anyhow!(format!(
2299 "Invalid federation member string {e:?}"
2300 )))
2301 })?;
2302
2303 let federation_id = invite_code.federation_id();
2304
2305 let mut federation_manager = self.federation_manager.write().await;
2306
2307 if federation_manager.has_federation(federation_id) {
2309 return Err(AdminGatewayError::ClientCreationError(anyhow!(
2310 "Federation has already been registered"
2311 )));
2312 }
2313
2314 let federation_index = federation_manager.pop_next_index()?;
2317
2318 let federation_config = FederationConfig {
2319 invite_code,
2320 federation_index,
2321 lightning_fee: self.default_routing_fees,
2322 transaction_fee: self.default_transaction_fees,
2323 _connector: ConnectorType::Tcp,
2325 };
2326
2327 let routing_fees = RoutingFees::try_from(federation_config.lightning_fee)
2331 .map_err(|err| AdminGatewayError::GatewayConfigurationError(err.to_string()))?;
2332
2333 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2334 .await
2335 .expect("mnemonic should be set");
2336 let recover = payload.recover.unwrap_or(false);
2337 if recover {
2338 self.client_builder
2339 .recover(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2340 .await?;
2341 }
2342
2343 let client = self
2344 .client_builder
2345 .build(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2346 .await?;
2347
2348 if recover {
2349 client.wait_for_all_active_state_machines().await?;
2350 }
2351
2352 let federation_info = FederationInfo {
2355 federation_id,
2356 federation_name: federation_manager.federation_name(&client).await,
2357 balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
2358 warn!(
2359 target: LOG_GATEWAY,
2360 err = %err.fmt_compact_anyhow(),
2361 %federation_id,
2362 "Balance not immediately available after joining/recovering."
2363 );
2364 Amount::default()
2365 }),
2366 config: federation_config.clone(),
2367 last_backup_time: None,
2368 };
2369
2370 Self::check_federation_network(&client, self.network).await?;
2371 if matches!(self.lightning_mode, LightningMode::Lnd { .. })
2372 && let Ok(lnv1) = client.get_first_module::<GatewayClientModule>()
2373 {
2374 for registration in self.registrations.values() {
2375 lnv1.try_register_with_federation(
2376 Vec::new(),
2378 GW_ANNOUNCEMENT_TTL,
2379 routing_fees,
2380 lightning_context.clone(),
2381 registration.endpoint_url.clone(),
2382 registration.keypair,
2383 )
2384 .await;
2385 }
2386 }
2387
2388 federation_manager.add_client(
2390 federation_index,
2391 Spanned::new(
2392 info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
2393 async { client },
2394 )
2395 .await,
2396 );
2397
2398 let mut dbtx = self.gateway_db.begin_transaction().await;
2399 dbtx.save_federation_config(&federation_config).await;
2400 dbtx.save_federation_backup_record(federation_id, None)
2401 .await;
2402 dbtx.commit_tx().await;
2403 debug!(
2404 target: LOG_GATEWAY,
2405 federation_id = %federation_id,
2406 federation_index = %federation_index,
2407 "Federation connected"
2408 );
2409
2410 Ok(federation_info)
2411 }
2412
2413 async fn handle_set_fees_msg(
2416 &self,
2417 SetFeesPayload {
2418 federation_id,
2419 lightning_base,
2420 lightning_parts_per_million,
2421 transaction_base,
2422 transaction_parts_per_million,
2423 }: SetFeesPayload,
2424 ) -> AdminResult<()> {
2425 let mut dbtx = self.gateway_db.begin_transaction().await;
2426 let mut fed_configs = if let Some(fed_id) = federation_id {
2427 dbtx.load_federation_configs()
2428 .await
2429 .into_iter()
2430 .filter(|(id, _)| *id == fed_id)
2431 .collect::<BTreeMap<_, _>>()
2432 } else {
2433 dbtx.load_federation_configs().await
2434 };
2435
2436 let federation_manager = self.federation_manager.read().await;
2437
2438 for (federation_id, config) in &mut fed_configs {
2439 let mut lightning_fee = config.lightning_fee;
2440 if let Some(lightning_base) = lightning_base {
2441 lightning_fee.base = lightning_base;
2442 }
2443
2444 if let Some(lightning_ppm) = lightning_parts_per_million {
2445 lightning_fee.parts_per_million = lightning_ppm;
2446 }
2447
2448 let mut transaction_fee = config.transaction_fee;
2449 if let Some(transaction_base) = transaction_base {
2450 transaction_fee.base = transaction_base;
2451 }
2452
2453 if let Some(transaction_ppm) = transaction_parts_per_million {
2454 transaction_fee.parts_per_million = transaction_ppm;
2455 }
2456
2457 federation_manager
2460 .client(federation_id)
2461 .ok_or(FederationNotConnected {
2462 federation_id_prefix: federation_id.to_prefix(),
2463 })?;
2464
2465 let send_fees = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
2471 AdminGatewayError::GatewayConfigurationError(format!(
2472 "Total Send fees overflowed, they may not exceed {}",
2473 PaymentFee::SEND_FEE_LIMIT
2474 ))
2475 })?;
2476
2477 if !send_fees.is_within(&PaymentFee::SEND_FEE_LIMIT) {
2479 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2480 "Total Send fees exceeded {}",
2481 PaymentFee::SEND_FEE_LIMIT
2482 )));
2483 }
2484
2485 if !transaction_fee.is_within(&PaymentFee::RECEIVE_FEE_LIMIT) {
2487 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2488 "Transaction fees exceeded RECEIVE LIMIT {}",
2489 PaymentFee::RECEIVE_FEE_LIMIT
2490 )));
2491 }
2492
2493 config.lightning_fee = lightning_fee;
2494 config.transaction_fee = transaction_fee;
2495 dbtx.save_federation_config(config).await;
2496 }
2497
2498 dbtx.commit_tx().await;
2499
2500 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2501 let register_task_group = TaskGroup::new();
2502
2503 self.register_federations(&fed_configs, ®ister_task_group)
2504 .await;
2505 }
2506
2507 Ok(())
2508 }
2509
2510 async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
2514 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2515 .await
2516 .expect("mnemonic should be set");
2517 let words = mnemonic
2518 .words()
2519 .map(std::string::ToString::to_string)
2520 .collect::<Vec<_>>();
2521 let all_federations = self
2522 .federation_manager
2523 .read()
2524 .await
2525 .get_all_federation_configs()
2526 .await
2527 .keys()
2528 .copied()
2529 .collect::<BTreeSet<_>>();
2530 let legacy_federations = self.client_builder.legacy_federations(all_federations);
2531 let mnemonic_response = MnemonicResponse {
2532 mnemonic: words,
2533 legacy_federations,
2534 };
2535 Ok(mnemonic_response)
2536 }
2537
2538 async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
2541 info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
2542 let context = self.get_lightning_context().await?;
2543 let res = context.lnrpc.open_channel(payload).await?;
2544 info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
2545 Txid::from_str(&res.funding_txid).map_err(|e| {
2546 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2547 failure_reason: format!("Received invalid channel funding txid string {e}"),
2548 })
2549 })
2550 }
2551
2552 async fn handle_connect_peer_msg(&self, payload: ConnectPeerRequest) -> AdminResult<()> {
2555 info!(
2556 target: LOG_GATEWAY,
2557 pubkey = %payload.node_address.pubkey,
2558 host = %payload.node_address.host_with_port(),
2559 "Connecting to Lightning peer..."
2560 );
2561 let context = self.get_lightning_context().await?;
2562 context.lnrpc.connect_peer(payload).await?;
2563 info!(target: LOG_GATEWAY, "Connected to Lightning peer");
2564 Ok(())
2565 }
2566
2567 async fn handle_close_channels_with_peer_msg(
2570 &self,
2571 payload: CloseChannelsWithPeerRequest,
2572 ) -> AdminResult<CloseChannelsWithPeerResponse> {
2573 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Closing lightning channel...");
2574 let context = self.get_lightning_context().await?;
2575 let response = context
2576 .lnrpc
2577 .close_channels_with_peer(payload.clone())
2578 .await?;
2579 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Initiated channel closure");
2580 Ok(response)
2581 }
2582
2583 async fn handle_set_channel_fees_msg(&self, payload: SetChannelFeesRequest) -> AdminResult<()> {
2586 info!(
2587 target: LOG_GATEWAY,
2588 funding_outpoint = %payload.funding_outpoint,
2589 base_fee_msat = payload.base_fee_msat,
2590 parts_per_million = payload.parts_per_million,
2591 "Updating channel fees..."
2592 );
2593 let context = self.get_lightning_context().await?;
2594 context.lnrpc.set_channel_fees(payload).await?;
2595 Ok(())
2596 }
2597
2598 async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
2601 let dbtx = self.gateway_db.begin_transaction_nc().await;
2602 let federation_infos = self
2603 .federation_manager
2604 .read()
2605 .await
2606 .federation_info_all_federations(dbtx)
2607 .await;
2608
2609 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
2610 .iter()
2611 .map(|federation_info| FederationBalanceInfo {
2612 federation_id: federation_info.federation_id,
2613 ecash_balance_msats: Amount {
2614 msats: federation_info.balance_msat.msats,
2615 },
2616 })
2617 .collect();
2618
2619 let context = self.get_lightning_context().await?;
2620 let lightning_node_balances = context.lnrpc.get_balances().await?;
2621
2622 Ok(GatewayBalances {
2623 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
2624 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
2625 ecash_balances,
2626 inbound_lightning_liquidity_msats: lightning_node_balances
2627 .inbound_lightning_liquidity_msats,
2628 })
2629 }
2630
2631 async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
2633 let context = self.get_lightning_context().await?;
2634 let response = context.lnrpc.send_onchain(payload.clone()).await?;
2635 let txid =
2636 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
2637 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
2638 })?;
2639 info!(onchain_request = %payload, txid = %txid, "Sent onchain transaction");
2640 Ok(txid)
2641 }
2642
2643 async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
2645 let context = self.get_lightning_context().await?;
2646 let response = context.lnrpc.get_ln_onchain_address().await?;
2647
2648 let address = Address::from_str(&response.address).map_err(|e| {
2649 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2650 failure_reason: e.to_string(),
2651 })
2652 })?;
2653
2654 address.require_network(self.network).map_err(|e| {
2655 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2656 failure_reason: e.to_string(),
2657 })
2658 })
2659 }
2660
2661 async fn handle_deposit_address_msg(
2662 &self,
2663 payload: DepositAddressPayload,
2664 ) -> AdminResult<Address> {
2665 self.handle_address_msg(payload).await
2666 }
2667
2668 async fn handle_receive_ecash_msg(
2669 &self,
2670 payload: ReceiveEcashPayload,
2671 ) -> AdminResult<ReceiveEcashResponse> {
2672 Self::handle_receive_ecash_msg(self, payload)
2673 .await
2674 .map_err(|e| AdminGatewayError::Unexpected(anyhow::anyhow!("{e}")))
2675 }
2676
2677 async fn handle_create_invoice_for_operator_msg(
2680 &self,
2681 payload: CreateInvoiceForOperatorPayload,
2682 ) -> AdminResult<Bolt11Invoice> {
2683 let GatewayState::Running { lightning_context } = self.get_state().await else {
2684 return Err(AdminGatewayError::Lightning(
2685 LightningRpcError::FailedToConnect,
2686 ));
2687 };
2688
2689 Bolt11Invoice::from_str(
2690 &lightning_context
2691 .lnrpc
2692 .create_invoice(CreateInvoiceRequest {
2693 payment_hash: None, amount_msat: payload.amount_msats,
2696 expiry_secs: payload.expiry_secs.unwrap_or(3600),
2697 description: payload.description.map(InvoiceDescription::Direct),
2698 })
2699 .await?
2700 .invoice,
2701 )
2702 .map_err(|e| {
2703 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2704 failure_reason: e.to_string(),
2705 })
2706 })
2707 }
2708
2709 async fn handle_pay_invoice_for_operator_msg(
2712 &self,
2713 payload: PayInvoiceForOperatorPayload,
2714 ) -> AdminResult<Preimage> {
2715 const BASE_FEE: u64 = 50;
2717 const FEE_DENOMINATOR: u64 = 100;
2718 const MAX_DELAY: u64 = 1008;
2719
2720 let GatewayState::Running { lightning_context } = self.get_state().await else {
2721 return Err(AdminGatewayError::Lightning(
2722 LightningRpcError::FailedToConnect,
2723 ));
2724 };
2725
2726 let max_fee = BASE_FEE
2727 + payload
2728 .invoice
2729 .amount_milli_satoshis()
2730 .context("Invoice is missing amount")?
2731 .saturating_div(FEE_DENOMINATOR);
2732
2733 let res = lightning_context
2734 .lnrpc
2735 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
2736 .await?;
2737 Ok(res.preimage)
2738 }
2739
2740 async fn handle_list_transactions_msg(
2742 &self,
2743 payload: ListTransactionsPayload,
2744 ) -> AdminResult<ListTransactionsResponse> {
2745 let lightning_context = self.get_lightning_context().await?;
2746 let response = lightning_context
2747 .lnrpc
2748 .list_transactions(payload.start_secs, payload.end_secs)
2749 .await?;
2750 Ok(response)
2751 }
2752
2753 async fn handle_spend_ecash_msg(
2755 &self,
2756 payload: SpendEcashPayload,
2757 ) -> AdminResult<SpendEcashResponse> {
2758 let client = self
2759 .select_client(payload.federation_id)
2760 .await?
2761 .into_value();
2762
2763 if let Ok(mint_module) = client.get_first_module::<MintClientModule>() {
2764 let notes = mint_module.send_oob_notes(payload.amount, ()).await?;
2765 debug!(target: LOG_GATEWAY, ?notes, "Spend ecash notes");
2766 Ok(SpendEcashResponse {
2767 notes: notes.to_string(),
2768 })
2769 } else if let Ok(mint_module) = client.get_first_module::<MintV2ClientModule>() {
2770 let (_, ecash) = mint_module
2771 .send(payload.amount, serde_json::Value::Null, true)
2772 .await
2773 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
2774
2775 Ok(SpendEcashResponse {
2776 notes: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
2777 })
2778 } else {
2779 Err(AdminGatewayError::Unexpected(anyhow::anyhow!(
2780 "No mint module available"
2781 )))
2782 }
2783 }
2784
2785 async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
2788 let was_running = {
2792 let mut state_guard = self.state.write().await;
2793 if let GatewayState::Running { lightning_context } = state_guard.clone() {
2794 *state_guard = GatewayState::ShuttingDown { lightning_context };
2795 true
2796 } else {
2797 false
2798 }
2799 };
2800
2801 if was_running {
2809 self.federation_manager
2810 .read()
2811 .await
2812 .wait_for_incoming_payments()
2813 .await?;
2814 }
2815
2816 let tg = task_group.clone();
2817 tg.spawn("Kill Gateway", |_task_handle| async {
2818 if let Err(err) = task_group.shutdown_join_all(Duration::from_mins(3)).await {
2819 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error shutting down gateway");
2820 }
2821 });
2822 Ok(())
2823 }
2824
2825 fn get_task_group(&self) -> TaskGroup {
2826 self.task_group.clone()
2827 }
2828
2829 async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> AdminResult<WithdrawResponse> {
2832 let WithdrawPayload {
2833 amount,
2834 address,
2835 federation_id,
2836 quoted_fees,
2837 } = payload;
2838
2839 let address_network = get_network_for_address(&address);
2840 let gateway_network = self.network;
2841 let Ok(address) = address.require_network(gateway_network) else {
2842 return Err(AdminGatewayError::WithdrawError {
2843 failure_reason: format!(
2844 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
2845 ),
2846 });
2847 };
2848
2849 let client = self.select_client(federation_id).await?;
2850
2851 if let Ok(wallet_module) = client
2852 .value()
2853 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
2854 {
2855 return withdraw_v2(client.value(), &wallet_module, &address, amount).await;
2856 }
2857
2858 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
2859
2860 let (withdraw_amount, fees) = match quoted_fees {
2863 Some(fees) => {
2865 let amt = match amount {
2866 BitcoinAmountOrAll::Amount(a) => a,
2867 BitcoinAmountOrAll::All => {
2868 return Err(AdminGatewayError::WithdrawError {
2870 failure_reason:
2871 "Cannot use 'all' with quoted fees - amount must be resolved first"
2872 .to_string(),
2873 });
2874 }
2875 };
2876 (amt, fees)
2877 }
2878 None => match amount {
2880 BitcoinAmountOrAll::All => {
2885 let balance = client.value().get_balance_for_btc().await.map_err(|err| {
2886 AdminGatewayError::Unexpected(anyhow!(
2887 "Balance not available: {}",
2888 err.fmt_compact_anyhow()
2889 ))
2890 })?;
2891
2892 wallet_module
2893 .max_withdrawable_amount(&address, balance)
2894 .await
2895 .map_err(|err| AdminGatewayError::WithdrawError {
2896 failure_reason: format!(
2897 "Insufficient funds. Balance: {balance}: {}",
2898 err.fmt_compact_anyhow()
2899 ),
2900 })?
2901 }
2902 BitcoinAmountOrAll::Amount(amount) => (
2903 amount,
2904 wallet_module.get_withdraw_fees(&address, amount).await?,
2905 ),
2906 },
2907 };
2908
2909 let operation_id = wallet_module
2910 .withdraw(&address, withdraw_amount, fees, ())
2911 .await?;
2912 let mut updates = wallet_module
2913 .subscribe_withdraw_updates(operation_id)
2914 .await?
2915 .into_stream();
2916
2917 while let Some(update) = updates.next().await {
2918 match update {
2919 WithdrawState::Succeeded(txid) => {
2920 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds");
2921 return Ok(WithdrawResponse { txid, fees });
2922 }
2923 WithdrawState::Failed(e) => {
2924 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
2925 }
2926 WithdrawState::Created => {}
2927 }
2928 }
2929
2930 Err(AdminGatewayError::WithdrawError {
2931 failure_reason: "Ran out of state updates while withdrawing".to_string(),
2932 })
2933 }
2934
2935 async fn handle_withdraw_preview_msg(
2938 &self,
2939 payload: WithdrawPreviewPayload,
2940 ) -> AdminResult<WithdrawPreviewResponse> {
2941 let gateway_network = self.network;
2942 let address_checked = payload
2943 .address
2944 .clone()
2945 .require_network(gateway_network)
2946 .map_err(|_| AdminGatewayError::WithdrawError {
2947 failure_reason: "Address network mismatch".to_string(),
2948 })?;
2949
2950 let client = self.select_client(payload.federation_id).await?;
2951
2952 let WithdrawDetails {
2953 amount,
2954 mint_fees,
2955 peg_out_fees,
2956 } = match payload.amount {
2957 BitcoinAmountOrAll::All => {
2958 calculate_max_withdrawable(client.value(), &address_checked).await?
2959 }
2960 BitcoinAmountOrAll::Amount(btc_amount) => {
2961 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
2962 WithdrawDetails {
2963 amount: btc_amount.into(),
2964 mint_fees: None,
2965 peg_out_fees: wallet_module
2966 .get_withdraw_fees(&address_checked, btc_amount)
2967 .await?,
2968 }
2969 } else if let Ok(wallet_module) = client
2970 .value()
2971 .get_first_module::<fedimint_walletv2_client::WalletClientModule>(
2972 ) {
2973 let fee = wallet_module.send_fee().await.map_err(|e| {
2974 AdminGatewayError::WithdrawError {
2975 failure_reason: e.to_string(),
2976 }
2977 })?;
2978 WithdrawDetails {
2979 amount: btc_amount.into(),
2980 mint_fees: None,
2981 peg_out_fees: PegOutFees::from_amount(fee),
2982 }
2983 } else {
2984 return Err(AdminGatewayError::Unexpected(anyhow!(
2985 "No wallet module found"
2986 )));
2987 }
2988 }
2989 };
2990
2991 let total_cost = amount
2992 .checked_add(peg_out_fees.amount().into())
2993 .and_then(|a| a.checked_add(mint_fees.unwrap_or(Amount::ZERO)))
2994 .ok_or_else(|| AdminGatewayError::Unexpected(anyhow!("Total cost overflow")))?;
2995
2996 Ok(WithdrawPreviewResponse {
2997 withdraw_amount: amount,
2998 address: payload.address.assume_checked().to_string(),
2999 peg_out_fees,
3000 total_cost,
3001 mint_fees,
3002 })
3003 }
3004
3005 async fn handle_payment_log_msg(
3017 &self,
3018 PaymentLogPayload {
3019 end_position,
3020 pagination_size,
3021 federation_id,
3022 event_kinds,
3023 }: PaymentLogPayload,
3024 ) -> AdminResult<PaymentLogResponse> {
3025 const BATCH_SIZE: u64 = 10_000;
3026 let federation_manager = self.federation_manager.read().await;
3027 let client = federation_manager
3028 .client(&federation_id)
3029 .ok_or(FederationNotConnected {
3030 federation_id_prefix: federation_id.to_prefix(),
3031 })?
3032 .value();
3033
3034 let event_kinds = if event_kinds.is_empty() {
3038 ALL_GATEWAY_EVENTS.to_vec()
3039 } else {
3040 event_kinds
3041 };
3042
3043 let end_position = if let Some(position) = end_position {
3044 position
3045 } else {
3046 let mut dbtx = client.db().begin_transaction_nc().await;
3047 dbtx.get_next_event_log_id().await
3048 };
3049
3050 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
3051
3052 let mut payment_log = Vec::new();
3053
3054 while payment_log.len() < pagination_size {
3055 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
3056 let mut filtered_batch = batch
3057 .into_iter()
3058 .filter(|e| e.id() <= end_position && event_kinds.contains(&e.as_raw().kind))
3059 .collect::<Vec<_>>();
3060 filtered_batch.reverse();
3061 payment_log.extend(filtered_batch);
3062
3063 start_position = start_position.saturating_sub(BATCH_SIZE);
3065
3066 if start_position == EventLogId::LOG_START {
3067 break;
3068 }
3069 }
3070
3071 payment_log.truncate(pagination_size);
3073
3074 Ok(PaymentLogResponse(payment_log))
3075 }
3076
3077 async fn handle_set_mnemonic_msg(&self, payload: SetMnemonicPayload) -> AdminResult<()> {
3080 let mut state_guard = self.state.write().await;
3085
3086 let GatewayState::NotConfigured { mnemonic_sender } = state_guard.clone() else {
3088 return Err(AdminGatewayError::MnemonicError(anyhow!(
3089 "Gateway is not is NotConfigured state"
3090 )));
3091 };
3092
3093 let mnemonic = if let Some(words) = payload.words {
3094 info!(target: LOG_GATEWAY, "Using user provided mnemonic");
3095 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(|e| {
3096 AdminGatewayError::MnemonicError(anyhow!(format!(
3097 "Seed phrase provided in environment was invalid {e:?}"
3098 )))
3099 })?
3100 } else {
3101 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
3102 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
3103 };
3104
3105 Client::store_encodable_client_secret(&self.gateway_db, mnemonic.to_entropy())
3106 .await
3107 .map_err(AdminGatewayError::MnemonicError)?;
3108
3109 *state_guard = GatewayState::Disconnected;
3110 drop(state_guard);
3111
3112 let _ = mnemonic_sender.send(());
3114
3115 Ok(())
3116 }
3117
3118 async fn handle_create_offer_for_operator_msg(
3120 &self,
3121 payload: CreateOfferPayload,
3122 ) -> AdminResult<CreateOfferResponse> {
3123 let lightning_context = self.get_lightning_context().await?;
3124 let offer = lightning_context.lnrpc.create_offer(
3125 payload.amount,
3126 payload.description,
3127 payload.expiry_secs,
3128 payload.quantity,
3129 )?;
3130 Ok(CreateOfferResponse { offer })
3131 }
3132
3133 async fn handle_pay_offer_for_operator_msg(
3135 &self,
3136 payload: PayOfferPayload,
3137 ) -> AdminResult<PayOfferResponse> {
3138 let lightning_context = self.get_lightning_context().await?;
3139 let preimage = lightning_context
3140 .lnrpc
3141 .pay_offer(
3142 payload.offer,
3143 payload.quantity,
3144 payload.amount,
3145 payload.payer_note,
3146 )
3147 .await?;
3148 Ok(PayOfferResponse {
3149 preimage: preimage.to_string(),
3150 })
3151 }
3152
3153 async fn handle_export_invite_codes(
3156 &self,
3157 ) -> BTreeMap<FederationId, BTreeMap<PeerId, (String, InviteCode)>> {
3158 let fed_manager = self.federation_manager.read().await;
3159 fed_manager.all_invite_codes().await
3160 }
3161
3162 async fn handle_get_note_summary_msg(
3165 &self,
3166 federation_id: &FederationId,
3167 ) -> AdminResult<TieredCounts> {
3168 let fed_manager = self.federation_manager.read().await;
3169 fed_manager.get_note_summary(federation_id).await
3170 }
3171
3172 fn get_password_hash(&self) -> String {
3173 self.bcrypt_password_hash.clone()
3174 }
3175
3176 fn gatewayd_version(&self) -> String {
3177 let gatewayd_version = env!("CARGO_PKG_VERSION");
3178 gatewayd_version.to_string()
3179 }
3180
3181 async fn get_chain_source(&self) -> (ChainSource, Network) {
3182 (self.chain_source.clone(), self.network)
3183 }
3184
3185 fn lightning_mode(&self) -> LightningMode {
3186 self.lightning_mode.clone()
3187 }
3188
3189 async fn is_configured(&self) -> bool {
3190 !matches!(self.get_state().await, GatewayState::NotConfigured { .. })
3191 }
3192}
3193
3194impl Gateway {
3196 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
3200 self.federation_manager
3201 .read()
3202 .await
3203 .client(federation_id)
3204 .and_then(|client| {
3205 client
3208 .value()
3209 .get_first_module::<GatewayClientModuleV2>()
3210 .ok()
3211 .map(|module| module.keypair.public_key())
3212 })
3213 }
3214
3215 pub async fn routing_info_v2(
3218 &self,
3219 federation_id: &FederationId,
3220 ) -> Result<Option<RoutingInfo>> {
3221 let context = self.get_lightning_context().await?;
3222
3223 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
3224 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
3225 PublicGatewayError::FederationNotConnected(FederationNotConnected {
3226 federation_id_prefix: federation_id.to_prefix(),
3227 }),
3228 )?;
3229
3230 let lightning_fee = fed_config.lightning_fee;
3231 let transaction_fee = fed_config.transaction_fee;
3232
3233 let send_fee_default = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
3236 PublicGatewayError::Unexpected(anyhow!(
3237 "The configured fees of federation {federation_id} cannot be added"
3238 ))
3239 })?;
3240
3241 Ok(self
3242 .public_key_v2(federation_id)
3243 .await
3244 .map(|module_public_key| RoutingInfo {
3245 lightning_public_key: context.lightning_public_key,
3246 lightning_alias: Some(context.lightning_alias.clone()),
3247 module_public_key,
3248 send_fee_default,
3249 send_fee_minimum: transaction_fee,
3253 expiration_delta_default: 1440,
3254 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
3255 receive_fee: transaction_fee,
3258 }))
3259 }
3260
3261 pub async fn send_payment_v2(
3264 &self,
3265 payload: SendPaymentPayload,
3266 ) -> Result<std::result::Result<[u8; 32], Signature>> {
3267 let client = self.select_client(payload.federation_id).await?;
3268 let module = client
3271 .value()
3272 .get_first_module::<GatewayClientModuleV2>()
3273 .map_err(|err| PublicGatewayError::LNv2(LNv2Error::OutgoingPayment(err)))?;
3274
3275 module
3276 .send_payment(payload)
3277 .await
3278 .map_err(LNv2Error::OutgoingPayment)
3279 .map_err(PublicGatewayError::LNv2)
3280 }
3281
3282 async fn create_bolt11_invoice_v2(
3287 &self,
3288 payload: CreateBolt11InvoicePayload,
3289 ) -> Result<Bolt11Invoice> {
3290 if !payload.contract.verify() {
3291 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3292 "The contract is invalid".to_string(),
3293 )));
3294 }
3295
3296 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
3297 LNv2Error::IncomingPayment(format!(
3298 "Federation {} does not exist",
3299 payload.federation_id
3300 )),
3301 )?;
3302
3303 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
3304 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3305 "The incoming contract is keyed to another gateway".to_string(),
3306 )));
3307 }
3308
3309 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
3310
3311 if contract_amount == Amount::ZERO {
3312 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3313 "Zero amount incoming contracts are not supported".to_string(),
3314 )));
3315 }
3316
3317 if contract_amount != payload.contract.commitment.amount {
3318 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3319 "The contract amount does not pay the correct amount of fees".to_string(),
3320 )));
3321 }
3322
3323 if payload.contract.commitment.expiration_or_fee <= duration_since_epoch().as_secs() {
3324 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3325 "The contract has already expired".to_string(),
3326 )));
3327 }
3328
3329 let payment_hash = match payload.contract.commitment.payment_image {
3330 PaymentImage::Hash(payment_hash) => payment_hash,
3331 PaymentImage::Point(..) => {
3332 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3333 "PaymentImage is not a payment hash".to_string(),
3334 )));
3335 }
3336 };
3337
3338 let invoice = self
3339 .create_invoice_via_lnrpc_v2(
3340 payment_hash,
3341 payload.amount,
3342 payload.description.clone(),
3343 payload.expiry_secs,
3344 )
3345 .await?;
3346
3347 let mut dbtx = self.gateway_db.begin_transaction().await;
3348
3349 if dbtx
3350 .save_registered_incoming_contract(
3351 payload.federation_id,
3352 payload.amount,
3353 payload.contract,
3354 )
3355 .await
3356 .is_some()
3357 {
3358 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3359 "PaymentHash is already registered".to_string(),
3360 )));
3361 }
3362
3363 dbtx.commit_tx_result().await.map_err(|_| {
3364 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3365 "Payment hash is already registered".to_string(),
3366 ))
3367 })?;
3368
3369 Ok(invoice)
3370 }
3371
3372 pub async fn create_invoice_via_lnrpc_v2(
3375 &self,
3376 payment_hash: sha256::Hash,
3377 amount: Amount,
3378 description: Bolt11InvoiceDescription,
3379 expiry_time: u32,
3380 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
3381 let lnrpc = self.get_lightning_context().await?.lnrpc;
3382
3383 let response = match description {
3384 Bolt11InvoiceDescription::Direct(description) => {
3385 lnrpc
3386 .create_invoice(CreateInvoiceRequest {
3387 payment_hash: Some(payment_hash),
3388 amount_msat: amount.msats,
3389 expiry_secs: expiry_time,
3390 description: Some(InvoiceDescription::Direct(description)),
3391 })
3392 .await?
3393 }
3394 Bolt11InvoiceDescription::Hash(hash) => {
3395 lnrpc
3396 .create_invoice(CreateInvoiceRequest {
3397 payment_hash: Some(payment_hash),
3398 amount_msat: amount.msats,
3399 expiry_secs: expiry_time,
3400 description: Some(InvoiceDescription::Hash(hash)),
3401 })
3402 .await?
3403 }
3404 };
3405
3406 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
3407 LightningRpcError::FailedToGetInvoice {
3408 failure_reason: e.to_string(),
3409 }
3410 })
3411 }
3412
3413 pub async fn verify_bolt11_preimage_v2(
3414 &self,
3415 payment_hash: sha256::Hash,
3416 wait: bool,
3417 ) -> std::result::Result<VerifyResponse, String> {
3418 let registered_contract = self
3419 .gateway_db
3420 .begin_transaction_nc()
3421 .await
3422 .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3423 .await
3424 .ok_or("Unknown payment hash".to_string())?;
3425
3426 let client = self
3427 .select_client(registered_contract.federation_id)
3428 .await
3429 .map_err(|_| "Not connected to federation".to_string())?
3430 .into_value();
3431
3432 let operation_id = OperationId::from_encodable(®istered_contract.contract);
3433
3434 if !(wait || client.operation_exists(operation_id).await) {
3435 return Ok(VerifyResponse {
3436 settled: false,
3437 preimage: None,
3438 });
3439 }
3440
3441 let module = client
3442 .get_first_module::<GatewayClientModuleV2>()
3443 .expect("Must have client module");
3444
3445 let Ok(state) = timeout(VERIFY_WAIT_TIMEOUT, module.await_receive(operation_id)).await
3446 else {
3447 return Ok(VerifyResponse {
3448 settled: false,
3449 preimage: None,
3450 });
3451 };
3452
3453 let preimage = match state {
3454 FinalReceiveState::Success(preimage) => Ok(preimage),
3455 FinalReceiveState::Failure => Err("Payment has failed".to_string()),
3456 FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
3457 FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
3458 }?;
3459
3460 Ok(VerifyResponse {
3461 settled: true,
3462 preimage: Some(preimage),
3463 })
3464 }
3465
3466 pub async fn get_registered_incoming_contract_and_client_v2(
3470 &self,
3471 payment_image: PaymentImage,
3472 amount_msats: u64,
3473 ) -> Result<(IncomingContract, ClientHandleArc)> {
3474 let registered_incoming_contract = self
3475 .gateway_db
3476 .begin_transaction_nc()
3477 .await
3478 .load_registered_incoming_contract(payment_image)
3479 .await
3480 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3481 "No corresponding decryption contract available".to_string(),
3482 )))?;
3483
3484 if registered_incoming_contract.incoming_amount_msats != amount_msats {
3485 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3486 "The available decryption contract's amount is not equal to the requested amount"
3487 .to_string(),
3488 )));
3489 }
3490
3491 let client = self
3492 .select_client(registered_incoming_contract.federation_id)
3493 .await?
3494 .into_value();
3495
3496 Ok((registered_incoming_contract.contract, client))
3497 }
3498}
3499
3500#[async_trait]
3501impl IGatewayClientV2 for Gateway {
3502 async fn complete_htlc(
3503 &self,
3504 htlc_response: InterceptPaymentResponse,
3505 ) -> std::result::Result<(), LightningRpcError> {
3506 loop {
3507 let lightning_context = self.await_lightning_context().await;
3508
3509 match lightning_context
3510 .lnrpc
3511 .complete_htlc(htlc_response.clone())
3512 .await
3513 {
3514 Ok(..) => return Ok(()),
3515 Err(err @ LightningRpcError::HtlcCompletionRejected { .. }) => {
3516 warn!(
3517 target: LOG_GATEWAY,
3518 err = %err.fmt_compact(),
3519 "Lightning cannot reach the requested terminal HTLC outcome",
3520 );
3521 return Err(err);
3522 }
3523 Err(err) => {
3524 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
3525 }
3526 }
3527
3528 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
3529 }
3530 }
3531
3532 async fn is_direct_swap(
3533 &self,
3534 invoice: &Bolt11Invoice,
3535 ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>> {
3536 let lightning_context = self.await_lightning_context().await;
3541 if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
3542 let (contract, client) = self
3543 .get_registered_incoming_contract_and_client_v2(
3544 PaymentImage::Hash(*invoice.payment_hash()),
3545 invoice
3546 .amount_milli_satoshis()
3547 .expect("The amount invoice has been previously checked"),
3548 )
3549 .await?;
3550 Ok(Some((contract, client)))
3551 } else {
3552 Ok(None)
3553 }
3554 }
3555
3556 async fn pay(
3557 &self,
3558 invoice: Bolt11Invoice,
3559 max_delay: u64,
3560 max_fee: Amount,
3561 ) -> std::result::Result<[u8; 32], LightningRpcError> {
3562 let lightning_context = self.await_lightning_context().await;
3565 lightning_context
3566 .lnrpc
3567 .pay(invoice, max_delay, max_fee)
3568 .await
3569 .map(|response| response.preimage.0)
3570 }
3571
3572 async fn min_contract_amount(
3573 &self,
3574 federation_id: &FederationId,
3575 amount: u64,
3576 ) -> anyhow::Result<Amount> {
3577 Ok(self
3578 .routing_info_v2(federation_id)
3579 .await?
3580 .ok_or(anyhow!("Routing Info not available"))?
3581 .send_fee_minimum
3582 .add_to(amount))
3583 }
3584
3585 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
3586 let rhints = invoice.route_hints();
3587 let hop = rhints.first().and_then(|rh| rh.0.last())?;
3588
3589 let lightning_context = self.await_lightning_context().await;
3592 if hop.src_node_id != lightning_context.lightning_public_key {
3593 return None;
3594 }
3595
3596 self.federation_manager
3597 .read()
3598 .await
3599 .get_client_for_index(hop.short_channel_id)
3600 }
3601
3602 async fn relay_lnv1_swap(
3603 &self,
3604 client: &ClientHandleArc,
3605 invoice: &Bolt11Invoice,
3606 ) -> anyhow::Result<FinalReceiveState> {
3607 let swap_params = SwapParameters {
3608 payment_hash: *invoice.payment_hash(),
3609 amount_msat: Amount::from_msats(
3610 invoice
3611 .amount_milli_satoshis()
3612 .ok_or(anyhow!("Amountless invoice not supported"))?,
3613 ),
3614 };
3615 let lnv1 = client
3616 .get_first_module::<GatewayClientModule>()
3617 .expect("No LNv1 module");
3618 let operation_id = lnv1.gateway_handle_direct_swap(swap_params).await?;
3619 let mut stream = lnv1
3620 .gateway_subscribe_ln_receive(operation_id)
3621 .await?
3622 .into_stream();
3623 let mut final_state = FinalReceiveState::Failure;
3624 while let Some(update) = stream.next().await {
3625 match update {
3626 GatewayExtReceiveStates::Funding => {}
3627 GatewayExtReceiveStates::FundingFailed { error: _ } => {
3628 final_state = FinalReceiveState::Rejected;
3629 }
3630 GatewayExtReceiveStates::Preimage(preimage) => {
3631 final_state = FinalReceiveState::Success(preimage.0);
3632 }
3633 GatewayExtReceiveStates::RefundError {
3634 error_message: _,
3635 error: _,
3636 } => {
3637 final_state = FinalReceiveState::Failure;
3638 }
3639 GatewayExtReceiveStates::RefundSuccess {
3640 out_points: _,
3641 error: _,
3642 } => {
3643 final_state = FinalReceiveState::Refunded;
3644 }
3645 }
3646 }
3647
3648 Ok(final_state)
3649 }
3650
3651 async fn claim_payment_image(
3652 &self,
3653 payment_image: &PaymentImage,
3654 operation_id: OperationId,
3655 ) -> bool {
3656 self.gateway_db
3660 .autocommit(
3661 |dbtx, _| {
3662 let payment_image = payment_image.clone();
3663 Box::pin(async move {
3664 let claimer = dbtx
3665 .claim_outgoing_payment_image(payment_image, operation_id)
3666 .await;
3667 Ok::<_, std::convert::Infallible>(claimer == operation_id)
3668 })
3669 },
3670 None,
3671 )
3672 .await
3673 .expect("Retries until the transaction commits")
3674 }
3675}
3676
3677#[async_trait]
3678impl IGatewayClientV1 for Gateway {
3679 async fn verify_preimage_authentication(
3680 &self,
3681 payment_hash: sha256::Hash,
3682 preimage_auth: sha256::Hash,
3683 contract: OutgoingContractAccount,
3684 ) -> std::result::Result<(), OutgoingPaymentError> {
3685 let mut dbtx = self.gateway_db.begin_transaction().await;
3686 if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
3687 if secret_hash != preimage_auth {
3688 return Err(OutgoingPaymentError {
3689 error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
3690 contract_id: contract.contract.contract_id(),
3691 contract: Some(contract),
3692 });
3693 }
3694 } else {
3695 dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
3698 .await;
3699 return dbtx
3700 .commit_tx_result()
3701 .await
3702 .map_err(|_| OutgoingPaymentError {
3703 error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
3704 contract_id: contract.contract.contract_id(),
3705 contract: Some(contract),
3706 });
3707 }
3708
3709 Ok(())
3710 }
3711
3712 async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()> {
3713 if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
3714 let lightning_context = self.get_lightning_context().await?;
3715
3716 ensure!(
3717 lightning_context.lnrpc.supports_private_payments(),
3718 "Private payments are not supported by the lightning node"
3719 );
3720 }
3721
3722 Ok(())
3723 }
3724
3725 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
3726 let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
3727 let lightning_fee = gateway_dbtx
3728 .load_federation_config(federation_id)
3729 .await?
3730 .lightning_fee;
3731
3732 RoutingFees::try_from(lightning_fee)
3736 .inspect_err(|err| {
3737 warn!(
3738 target: LOG_GATEWAY,
3739 %federation_id,
3740 err = %err.fmt_compact(),
3741 "Configured lightning fee cannot be used. Set a smaller fee with `set_fees`."
3742 );
3743 })
3744 .ok()
3745 }
3746
3747 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
3748 self.federation_manager
3749 .read()
3750 .await
3751 .client(federation_id)
3752 .cloned()
3753 }
3754
3755 async fn get_client_for_invoice(
3756 &self,
3757 payment_data: PaymentData,
3758 ) -> Option<Spanned<ClientHandleArc>> {
3759 let rhints = payment_data.route_hints();
3760 let hop = rhints.first().and_then(|rh| rh.0.last())?;
3761
3762 let lightning_context = self.await_lightning_context().await;
3765 if hop.src_node_id != lightning_context.lightning_public_key {
3766 return None;
3767 }
3768
3769 self.federation_manager
3770 .read()
3771 .await
3772 .get_client_for_index(hop.short_channel_id)
3773 }
3774
3775 async fn pay(
3776 &self,
3777 payment_data: PaymentData,
3778 max_delay: u64,
3779 max_fee: Amount,
3780 ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
3781 let lightning_context = self.await_lightning_context().await;
3784
3785 match payment_data {
3786 PaymentData::Invoice(invoice) => {
3787 lightning_context
3788 .lnrpc
3789 .pay(invoice, max_delay, max_fee)
3790 .await
3791 }
3792 PaymentData::PrunedInvoice(invoice) => {
3793 lightning_context
3794 .lnrpc
3795 .pay_private(invoice, max_delay, max_fee)
3796 .await
3797 }
3798 }
3799 }
3800
3801 async fn complete_htlc(
3802 &self,
3803 htlc: InterceptPaymentResponse,
3804 ) -> std::result::Result<(), LightningRpcError> {
3805 let lightning_context = self.await_lightning_context().await;
3807
3808 lightning_context.lnrpc.complete_htlc(htlc).await
3809 }
3810
3811 async fn is_lnv2_direct_swap(
3812 &self,
3813 payment_hash: sha256::Hash,
3814 amount: Amount,
3815 ) -> anyhow::Result<
3816 Option<(
3817 fedimint_lnv2_common::contracts::IncomingContract,
3818 ClientHandleArc,
3819 )>,
3820 > {
3821 let (contract, client) = self
3822 .get_registered_incoming_contract_and_client_v2(
3823 PaymentImage::Hash(payment_hash),
3824 amount.msats,
3825 )
3826 .await?;
3827 Ok(Some((contract, client)))
3828 }
3829}