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