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