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, SetFeesPayload, SetMnemonicPayload, SpendEcashPayload,
85 SpendEcashResponse, V1_API_ENDPOINT, WithdrawPayload, WithdrawPreviewPayload,
86 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
145const LIGHTNING_CONTEXT_RETRY_INTERVAL: Duration = Duration::from_secs(5);
148
149pub type Result<T> = std::result::Result<T, PublicGatewayError>;
150pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
151
152const DB_FILE: &str = "gatewayd.db";
155
156const LDK_NODE_DB_FOLDER: &str = "ldk_node";
159
160#[cfg_attr(doc, aquamarine::aquamarine)]
161#[derive(Clone, Debug)]
174pub enum GatewayState {
175 NotConfigured {
176 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
179 },
180 Disconnected,
181 Syncing,
182 Connected,
183 Running {
184 lightning_context: LightningContext,
185 },
186 ShuttingDown {
187 lightning_context: LightningContext,
188 },
189}
190
191impl Display for GatewayState {
192 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
193 match self {
194 GatewayState::NotConfigured { .. } => write!(f, "NotConfigured"),
195 GatewayState::Disconnected => write!(f, "Disconnected"),
196 GatewayState::Syncing => write!(f, "Syncing"),
197 GatewayState::Connected => write!(f, "Connected"),
198 GatewayState::Running { .. } => write!(f, "Running"),
199 GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"),
200 }
201 }
202}
203
204#[derive(Debug, Clone)]
207struct Registration {
208 endpoint_url: SafeUrl,
210
211 keypair: secp256k1::Keypair,
213}
214
215impl Registration {
216 pub async fn new(db: &Database, endpoint_url: SafeUrl, protocol: RegisteredProtocol) -> Self {
217 let keypair = Gateway::load_or_create_gateway_keypair(db, protocol).await;
218 Self {
219 endpoint_url,
220 keypair,
221 }
222 }
223}
224
225#[bon::bon]
226impl Gateway {
227 #[builder(start_fn = builder, finish_fn = build)]
242 pub async fn new_with_builder(
243 #[builder(start_fn)] lightning_mode: LightningMode,
244 #[builder(start_fn)] client_builder: GatewayClientBuilder,
245 #[builder(start_fn)] gateway_db: Database,
246 bcrypt_password_hash: bcrypt::HashParts,
247 bcrypt_liquidity_manager_password_hash: Option<bcrypt::HashParts>,
248 gateway_state: GatewayState,
249 chain_source: ChainSource,
250 #[builder(default = ([127, 0, 0, 1], 80).into())] listen: SocketAddr,
251 api_addr: Option<SafeUrl>,
252 #[builder(default = DEFAULT_NETWORK)] network: Network,
253 #[builder(default = DEFAULT_NUM_ROUTE_HINTS)] num_route_hints: u32,
254 #[builder(default = PaymentFee::TRANSACTION_FEE_DEFAULT)] default_routing_fees: PaymentFee,
255 #[builder(default = PaymentFee::TRANSACTION_FEE_DEFAULT)]
256 default_transaction_fees: PaymentFee,
257 iroh_listen: Option<SocketAddr>,
258 iroh_dns: Option<SafeUrl>,
259 #[builder(default)] iroh_relays: Vec<SafeUrl>,
260 metrics_listen: Option<SocketAddr>,
261 ) -> anyhow::Result<Gateway> {
262 let versioned_api = api_addr.map(|addr| {
263 addr.join(V1_API_ENDPOINT)
264 .expect("Failed to version gateway API address")
265 });
266
267 let metrics_listen = metrics_listen.unwrap_or_else(|| {
268 SocketAddr::new(
269 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
270 listen.port() + 1,
271 )
272 });
273
274 Gateway::new(
275 lightning_mode,
276 GatewayParameters {
277 listen,
278 versioned_api,
279 bcrypt_password_hash,
280 bcrypt_liquidity_manager_password_hash,
281 network,
282 num_route_hints,
283 default_routing_fees,
284 default_transaction_fees,
285 iroh_listen,
286 iroh_dns,
287 iroh_relays,
288 skip_setup: true,
289 metrics_listen,
290 },
291 gateway_db,
292 client_builder,
293 gateway_state,
294 chain_source,
295 )
296 .await
297 }
298}
299
300enum ReceivePaymentStreamAction {
302 RetryAfterDelay,
303 NoRetry,
304}
305
306#[derive(Clone)]
307pub struct Gateway {
308 federation_manager: Arc<RwLock<FederationManager>>,
310
311 lightning_mode: LightningMode,
313
314 state: Arc<RwLock<GatewayState>>,
316
317 client_builder: GatewayClientBuilder,
320
321 gateway_db: Database,
323
324 listen: SocketAddr,
326
327 metrics_listen: SocketAddr,
329
330 task_group: TaskGroup,
332
333 bcrypt_password_hash: String,
335
336 bcrypt_liquidity_manager_password_hash: Option<String>,
339
340 num_route_hints: u32,
342
343 network: Network,
345
346 chain_source: ChainSource,
348
349 default_routing_fees: PaymentFee,
351
352 default_transaction_fees: PaymentFee,
354
355 iroh_sk: iroh::SecretKey,
357
358 iroh_listen: Option<SocketAddr>,
360
361 iroh_dns: Option<SafeUrl>,
363
364 iroh_relays: Vec<SafeUrl>,
367
368 registrations: BTreeMap<RegisteredProtocol, Registration>,
371}
372
373impl std::fmt::Debug for Gateway {
374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 f.debug_struct("Gateway")
376 .field("federation_manager", &self.federation_manager)
377 .field("state", &self.state)
378 .field("client_builder", &self.client_builder)
379 .field("gateway_db", &self.gateway_db)
380 .field("listen", &self.listen)
381 .field("registrations", &self.registrations)
382 .finish_non_exhaustive()
383 }
384}
385
386struct WithdrawDetails {
388 amount: Amount,
389 mint_fees: Option<Amount>,
390 peg_out_fees: PegOutFees,
391}
392
393async fn withdraw_v2(
395 client: &ClientHandleArc,
396 wallet_module: &fedimint_walletv2_client::WalletClientModule,
397 address: &Address,
398 amount: BitcoinAmountOrAll,
399) -> AdminResult<WithdrawResponse> {
400 let fee = wallet_module
401 .send_fee()
402 .await
403 .map_err(|e| AdminGatewayError::WithdrawError {
404 failure_reason: e.to_string(),
405 })?;
406
407 let withdraw_amount = match amount {
408 BitcoinAmountOrAll::All => {
409 let balance = bitcoin::Amount::from_sat(
410 client
411 .get_balance_for_btc()
412 .await
413 .map_err(|err| {
414 AdminGatewayError::Unexpected(anyhow!(
415 "Balance not available: {}",
416 err.fmt_compact_anyhow()
417 ))
418 })?
419 .msats
420 / 1000,
421 );
422 balance
423 .checked_sub(fee)
424 .ok_or_else(|| AdminGatewayError::WithdrawError {
425 failure_reason: format!("Insufficient funds. Balance: {balance} Fee: {fee}"),
426 })?
427 }
428 BitcoinAmountOrAll::Amount(a) => a,
429 };
430
431 let operation_id = wallet_module
432 .send(address.as_unchecked().clone(), withdraw_amount, Some(fee))
433 .await
434 .map_err(|e| AdminGatewayError::WithdrawError {
435 failure_reason: e.to_string(),
436 })?;
437
438 let result = wallet_module
439 .await_final_send_operation_state(operation_id)
440 .await
441 .map_err(|e| AdminGatewayError::WithdrawError {
442 failure_reason: e.to_string(),
443 })?;
444
445 let fees = PegOutFees::from_amount(fee);
446
447 match result {
448 fedimint_walletv2_client::FinalSendOperationState::Success(txid) => {
449 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds via walletv2");
450 Ok(WithdrawResponse { txid, fees })
451 }
452 fedimint_walletv2_client::FinalSendOperationState::Aborted => {
453 Err(AdminGatewayError::WithdrawError {
454 failure_reason: "Withdrawal transaction was aborted".to_string(),
455 })
456 }
457 fedimint_walletv2_client::FinalSendOperationState::Failure => {
458 Err(AdminGatewayError::WithdrawError {
459 failure_reason: "Withdrawal failed".to_string(),
460 })
461 }
462 }
463}
464
465async fn calculate_max_withdrawable(
467 client: &ClientHandleArc,
468 address: &Address,
469) -> AdminResult<WithdrawDetails> {
470 let balance = client.get_balance_for_btc().await.map_err(|err| {
471 AdminGatewayError::Unexpected(anyhow!(
472 "Balance not available: {}",
473 err.fmt_compact_anyhow()
474 ))
475 })?;
476
477 let peg_out_fees = if let Ok(wallet_module) = client.get_first_module::<WalletClientModule>() {
478 wallet_module
479 .get_withdraw_fees(
480 address,
481 bitcoin::Amount::from_sat(balance.sats_round_down()),
482 )
483 .await?
484 } else if let Ok(wallet_module) =
485 client.get_first_module::<fedimint_walletv2_client::WalletClientModule>()
486 {
487 let fee = wallet_module
488 .send_fee()
489 .await
490 .map_err(|e| AdminGatewayError::WithdrawError {
491 failure_reason: e.to_string(),
492 })?;
493 PegOutFees::from_amount(fee)
494 } else {
495 return Err(AdminGatewayError::Unexpected(anyhow!(
496 "No wallet module found"
497 )));
498 };
499
500 let max_withdrawable_before_mint_fees = balance
501 .checked_sub(peg_out_fees.amount().into())
502 .ok_or_else(|| AdminGatewayError::WithdrawError {
503 failure_reason: "Insufficient balance to cover peg-out fees".to_string(),
504 })?;
505
506 let mint_fees = if let Ok(mint_module) = client.get_first_module::<MintClientModule>() {
508 mint_module.estimate_spend_all_fees().await
509 } else {
510 Amount::ZERO
511 };
512
513 let max_withdrawable = max_withdrawable_before_mint_fees.saturating_sub(mint_fees);
514
515 Ok(WithdrawDetails {
516 amount: max_withdrawable,
517 mint_fees: Some(mint_fees),
518 peg_out_fees,
519 })
520}
521
522impl Gateway {
523 fn get_bitcoind_client(
526 opts: &GatewayOpts,
527 network: bitcoin::Network,
528 gateway_id: &PublicKey,
529 ) -> anyhow::Result<(BitcoindClient, ChainSource)> {
530 let bitcoind_username = opts
531 .bitcoind_username
532 .clone()
533 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
534 let url = opts.bitcoind_url.clone().expect("No bitcoind url set");
535 let password = opts
536 .bitcoind_password
537 .clone()
538 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
539
540 let chain_source = ChainSource::Bitcoind {
541 username: bitcoind_username.clone(),
542 password: password.clone(),
543 server_url: url.clone(),
544 };
545 let wallet_name = format!("gatewayd-{gateway_id}");
546 let client = BitcoindClient::new(&url, bitcoind_username, password, &wallet_name, network)?;
547 Ok((client, chain_source))
548 }
549
550 pub async fn new_with_default_modules(
553 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
554 ) -> anyhow::Result<Gateway> {
555 let opts = GatewayOpts::parse();
556 let gateway_parameters = opts.to_gateway_parameters()?;
557 let decoders = ModuleDecoderRegistry::default();
558
559 let db_path = opts.data_dir.join(DB_FILE);
560 let gateway_db = match opts.db_backend {
561 DatabaseBackend::RocksDb => {
562 debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
563 Database::new(
564 fedimint_rocksdb::RocksDb::build(db_path).open().await?,
565 decoders,
566 )
567 }
568 DatabaseBackend::CursedRedb => {
569 debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
570 Database::new(
571 fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
572 decoders,
573 )
574 }
575 };
576
577 apply_migrations(
580 &gateway_db,
581 (),
582 "gatewayd".to_string(),
583 get_gatewayd_database_migrations(),
584 None,
585 None,
586 )
587 .await?;
588
589 let http_id = Self::load_or_create_gateway_keypair(&gateway_db, RegisteredProtocol::Http)
592 .await
593 .public_key();
594 let (dyn_bitcoin_rpc, chain_source) =
595 match (opts.bitcoind_url.as_ref(), opts.esplora_url.as_ref()) {
596 (Some(_), None) => {
597 let (client, chain_source) =
598 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
599 (client.into_dyn(), chain_source)
600 }
601 (None, Some(url)) => {
602 let client = EsploraClient::new(url)
603 .expect("Could not create EsploraClient")
604 .into_dyn();
605 let chain_source = ChainSource::Esplora {
606 server_url: url.clone(),
607 };
608 (client, chain_source)
609 }
610 (Some(_), Some(_)) => {
611 let (client, chain_source) =
613 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
614 (client.into_dyn(), chain_source)
615 }
616 _ => unreachable!("ArgGroup already enforced XOR relation"),
617 };
618
619 let mut registry = ClientModuleInitRegistry::new();
622 registry.attach(MintClientInit);
623 registry.attach(MintV2ClientInit);
624 registry.attach(WalletClientInit::new(dyn_bitcoin_rpc));
625 registry.attach(fedimint_walletv2_client::WalletClientInit);
626
627 let client_builder =
628 GatewayClientBuilder::new(opts.data_dir.clone(), registry, opts.db_backend).await?;
629
630 let gateway_state = if Self::load_mnemonic(&gateway_db).await.is_some() {
631 GatewayState::Disconnected
632 } else {
633 if gateway_parameters.skip_setup {
636 let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
637 info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
638 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
639 |e| {
640 AdminGatewayError::MnemonicError(anyhow!(format!(
641 "Seed phrase provided in environment was invalid {e:?}"
642 )))
643 },
644 )?
645 } else {
646 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
647 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
648 };
649
650 Client::store_encodable_client_secret(&gateway_db, mnemonic.to_entropy())
651 .await
652 .map_err(AdminGatewayError::MnemonicError)?;
653 GatewayState::Disconnected
654 } else {
655 GatewayState::NotConfigured { mnemonic_sender }
656 }
657 };
658
659 info!(
660 target: LOG_GATEWAY,
661 version = %fedimint_build_code_version_env!(),
662 "Starting gatewayd",
663 );
664
665 Gateway::new(
666 opts.mode,
667 gateway_parameters,
668 gateway_db,
669 client_builder,
670 gateway_state,
671 chain_source,
672 )
673 .await
674 }
675
676 async fn new(
679 lightning_mode: LightningMode,
680 gateway_parameters: GatewayParameters,
681 gateway_db: Database,
682 client_builder: GatewayClientBuilder,
683 gateway_state: GatewayState,
684 chain_source: ChainSource,
685 ) -> anyhow::Result<Gateway> {
686 let num_route_hints = gateway_parameters.num_route_hints;
687 let network = gateway_parameters.network;
688
689 let task_group = TaskGroup::new();
690 task_group.install_kill_handler();
691
692 let mut registrations = BTreeMap::new();
693 if let Some(http_url) = gateway_parameters.versioned_api {
694 registrations.insert(
695 RegisteredProtocol::Http,
696 Registration::new(&gateway_db, http_url, RegisteredProtocol::Http).await,
697 );
698 }
699
700 let iroh_sk = Self::load_or_create_iroh_key(&gateway_db).await;
701 if gateway_parameters.iroh_listen.is_some() {
702 let endpoint_url = SafeUrl::parse(&format!("iroh://{}", iroh_sk.public()))?;
703 registrations.insert(
704 RegisteredProtocol::Iroh,
705 Registration::new(&gateway_db, endpoint_url, RegisteredProtocol::Iroh).await,
706 );
707 }
708
709 Ok(Self {
710 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
711 lightning_mode,
712 state: Arc::new(RwLock::new(gateway_state)),
713 client_builder,
714 gateway_db: gateway_db.clone(),
715 listen: gateway_parameters.listen,
716 metrics_listen: gateway_parameters.metrics_listen,
717 task_group,
718 bcrypt_password_hash: gateway_parameters.bcrypt_password_hash.to_string(),
719 bcrypt_liquidity_manager_password_hash: gateway_parameters
720 .bcrypt_liquidity_manager_password_hash
721 .map(|h| h.to_string()),
722 num_route_hints,
723 network,
724 chain_source,
725 default_routing_fees: gateway_parameters.default_routing_fees,
726 default_transaction_fees: gateway_parameters.default_transaction_fees,
727 iroh_sk,
728 iroh_dns: gateway_parameters.iroh_dns,
729 iroh_relays: gateway_parameters.iroh_relays,
730 iroh_listen: gateway_parameters.iroh_listen,
731 registrations,
732 })
733 }
734
735 async fn load_or_create_gateway_keypair(
736 gateway_db: &Database,
737 protocol: RegisteredProtocol,
738 ) -> secp256k1::Keypair {
739 let mut dbtx = gateway_db.begin_transaction().await;
740 let keypair = dbtx.load_or_create_gateway_keypair(protocol).await;
741 dbtx.commit_tx().await;
742 keypair
743 }
744
745 async fn load_or_create_iroh_key(gateway_db: &Database) -> iroh::SecretKey {
748 let mut dbtx = gateway_db.begin_transaction().await;
749 let iroh_sk = dbtx.load_or_create_iroh_key().await;
750 dbtx.commit_tx().await;
751 iroh_sk
752 }
753
754 pub async fn http_gateway_id(&self) -> PublicKey {
755 Self::load_or_create_gateway_keypair(&self.gateway_db, RegisteredProtocol::Http)
756 .await
757 .public_key()
758 }
759
760 async fn get_state(&self) -> GatewayState {
761 self.state.read().await.clone()
762 }
763
764 pub async fn dump_database(
767 dbtx: &mut DatabaseTransaction<'_>,
768 prefix_names: Vec<String>,
769 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
770 dbtx.dump_database(prefix_names).await
771 }
772
773 pub async fn run(
778 self,
779 runtime: Arc<tokio::runtime::Runtime>,
780 mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
781 ) -> anyhow::Result<TaskShutdownToken> {
782 install_crypto_provider().await;
783 self.register_clients_timer();
784 self.load_clients().await?;
785 self.start_gateway(runtime, mnemonic_receiver.resubscribe());
786 self.spawn_backup_task();
787 fedimint_metrics::spawn_api_server(self.metrics_listen, self.task_group.clone()).await?;
789 let handle = self.task_group.make_handle();
791 run_webserver(Arc::new(self), mnemonic_receiver.resubscribe()).await?;
792 let shutdown_receiver = handle.make_shutdown_rx();
793 Ok(shutdown_receiver)
794 }
795
796 fn spawn_backup_task(&self) {
799 let self_copy = self.clone();
800 self.task_group
801 .spawn_cancellable_silent("backup ecash", async move {
802 const BACKUP_UPDATE_INTERVAL: Duration = Duration::from_hours(1);
803 let mut interval = tokio::time::interval(BACKUP_UPDATE_INTERVAL);
804 interval.tick().await;
805 loop {
806 {
807 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
808 self_copy.backup_all_federations(&mut dbtx).await;
809 dbtx.commit_tx().await;
810 interval.tick().await;
811 }
812 }
813 });
814 }
815
816 pub async fn backup_all_federations(&self, dbtx: &mut DatabaseTransaction<'_, Committable>) {
820 const BACKUP_THRESHOLD_DURATION: Duration = Duration::from_hours(24);
823
824 let now = fedimint_core::time::now();
825 let threshold = now
826 .checked_sub(BACKUP_THRESHOLD_DURATION)
827 .expect("Cannot be negative");
828 for (id, last_backup) in dbtx.load_backup_records().await {
829 match last_backup {
830 Some(backup_time) if backup_time < threshold => {
831 let fed_manager = self.federation_manager.read().await;
832 fed_manager.backup_federation(&id, dbtx, now).await;
833 }
834 None => {
835 let fed_manager = self.federation_manager.read().await;
836 fed_manager.backup_federation(&id, dbtx, now).await;
837 }
838 _ => {}
839 }
840 }
841 }
842
843 fn start_gateway(
846 &self,
847 runtime: Arc<tokio::runtime::Runtime>,
848 mut mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
849 ) {
850 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 60;
851
852 let self_copy = self.clone();
853 let tg = self.task_group.clone();
854 self.task_group.spawn(
855 "Subscribe to intercepted lightning payments in stream",
856 |handle| async move {
857 loop {
859 if handle.is_shutting_down() {
860 info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
861 break;
862 }
863
864 if let GatewayState::NotConfigured{ .. } = self_copy.get_state().await {
865 info!(
866 target: LOG_GATEWAY,
867 "Waiting for the mnemonic to be set before starting lightning receive loop."
868 );
869 info!(
870 target: LOG_GATEWAY,
871 "You might need to provide it from the UI or refer to documentation w.r.t how to initialize it."
872 );
873
874 let _ = mnemonic_receiver.recv().await;
875 info!(
876 target: LOG_GATEWAY,
877 "Received mnemonic, attempting to start lightning receive loop"
878 );
879 }
880
881 let payment_stream_task_group = tg.make_subgroup();
882 let lnrpc_route = self_copy.create_lightning_client(runtime.clone()).await;
883
884 debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
885 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
886 {
887 Ok((stream, ln_client)) => (stream, ln_client),
888 Err(err) => {
889 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
890 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
898 crit!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Lightning payment stream task group shutdown");
899 }
900 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
901 continue
902 }
903 };
904
905 self_copy.set_gateway_state(GatewayState::Connected).await;
907 info!(target: LOG_GATEWAY, "Established lightning payment stream");
908
909 let route_payments_response =
910 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
911
912 self_copy.set_gateway_state(GatewayState::Disconnected).await;
913 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
914 crit!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Lightning payment stream task group shutdown");
915 }
916
917 self_copy.unannounce_from_all_federations().await;
918
919 match route_payments_response {
920 ReceivePaymentStreamAction::RetryAfterDelay => {
921 warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
922 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
923 }
924 ReceivePaymentStreamAction::NoRetry => break,
925 }
926 }
927 },
928 );
929 }
930
931 async fn route_lightning_payments<'a>(
935 &'a self,
936 handle: &TaskHandle,
937 mut stream: RouteHtlcStream<'a>,
938 ln_client: Arc<dyn ILnRpcClient>,
939 ) -> ReceivePaymentStreamAction {
940 let LightningInfo::Connected {
941 public_key: lightning_public_key,
942 alias: lightning_alias,
943 network: lightning_network,
944 block_height: _,
945 synced_to_chain,
946 } = ln_client.parsed_node_info().await
947 else {
948 warn!(target: LOG_GATEWAY, "Failed to retrieve Lightning info");
949 return ReceivePaymentStreamAction::RetryAfterDelay;
950 };
951
952 assert!(
953 self.network == lightning_network,
954 "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
955 self.network
956 );
957
958 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
959 info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
960 } else {
961 self.set_gateway_state(GatewayState::Syncing).await;
962 info!(target: LOG_GATEWAY, "Waiting for chain sync");
963 if let Err(err) = ln_client.wait_for_chain_sync().await {
964 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
965 return ReceivePaymentStreamAction::RetryAfterDelay;
966 }
967 }
968
969 let lightning_context = LightningContext {
970 lnrpc: LnRpcTracked::new(ln_client, "gateway"),
971 lightning_public_key,
972 lightning_alias,
973 lightning_network,
974 };
975 self.set_gateway_state(GatewayState::Running { lightning_context })
976 .await;
977 info!(target: LOG_GATEWAY, "Gateway is running");
978
979 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
980 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
983 let all_federations_configs =
984 dbtx.load_federation_configs().await.into_iter().collect();
985 self.register_federations(&all_federations_configs, &self.task_group)
986 .await;
987 }
988
989 let htlc_task_group = self.task_group.make_subgroup();
992 if handle
993 .cancel_on_shutdown(async move {
994 loop {
995 let payment_request_or = tokio::select! {
996 payment_request_or = stream.next() => {
997 payment_request_or
998 }
999 () = self.is_shutting_down_safely() => {
1000 break;
1001 }
1002 };
1003
1004 let Some(payment_request) = payment_request_or else {
1005 warn!(
1006 target: LOG_GATEWAY,
1007 "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
1008 );
1009 break;
1010 };
1011
1012 let state_guard = self.state.read().await;
1013 if let GatewayState::Running { ref lightning_context } = *state_guard {
1014 let gateway = self.clone();
1016 let lightning_context = lightning_context.clone();
1017 htlc_task_group.spawn_cancellable_silent(
1018 "handle_lightning_payment",
1019 async move {
1020 let start = fedimint_core::time::now();
1021 let outcome = gateway
1022 .handle_lightning_payment(payment_request, &lightning_context)
1023 .await;
1024 metrics::HTLC_HANDLING_DURATION_SECONDS
1025 .with_label_values(&[outcome])
1026 .observe(
1027 fedimint_core::time::now()
1028 .duration_since(start)
1029 .unwrap_or_default()
1030 .as_secs_f64(),
1031 );
1032 },
1033 );
1034 } else {
1035 warn!(
1036 target: LOG_GATEWAY,
1037 state = %state_guard,
1038 "Gateway isn't in a running state, cannot handle incoming payments."
1039 );
1040 break;
1041 }
1042 }
1043 })
1044 .await
1045 .is_ok()
1046 {
1047 warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
1048 ReceivePaymentStreamAction::RetryAfterDelay
1049 } else {
1050 info!(target: LOG_GATEWAY, "Received shutdown signal");
1051 ReceivePaymentStreamAction::NoRetry
1052 }
1053 }
1054
1055 async fn is_shutting_down_safely(&self) {
1058 loop {
1059 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
1060 return;
1061 }
1062
1063 fedimint_core::task::sleep(Duration::from_secs(1)).await;
1064 }
1065 }
1066
1067 async fn handle_lightning_payment(
1077 &self,
1078 payment_request: InterceptPaymentRequest,
1079 lightning_context: &LightningContext,
1080 ) -> &'static str {
1081 info!(
1082 target: LOG_GATEWAY,
1083 lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
1084 "Intercepting lightning payment",
1085 );
1086
1087 let lnv2_start = fedimint_core::time::now();
1088 let lnv2_result = self
1089 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
1090 .await;
1091 let lnv2_outcome = if lnv2_result.is_ok() {
1092 "success"
1093 } else {
1094 "error"
1095 };
1096 metrics::HTLC_LNV2_ATTEMPT_DURATION_SECONDS
1097 .with_label_values(&[lnv2_outcome])
1098 .observe(
1099 fedimint_core::time::now()
1100 .duration_since(lnv2_start)
1101 .unwrap_or_default()
1102 .as_secs_f64(),
1103 );
1104 if lnv2_result.is_ok() {
1105 return "lnv2";
1106 }
1107
1108 let lnv1_start = fedimint_core::time::now();
1109 let lnv1_result = self
1110 .try_handle_lightning_payment_ln_legacy(&payment_request, lightning_context)
1111 .await;
1112 let lnv1_outcome = if lnv1_result.is_ok() {
1113 "success"
1114 } else {
1115 "error"
1116 };
1117 metrics::HTLC_LNV1_ATTEMPT_DURATION_SECONDS
1118 .with_label_values(&[lnv1_outcome])
1119 .observe(
1120 fedimint_core::time::now()
1121 .duration_since(lnv1_start)
1122 .unwrap_or_default()
1123 .as_secs_f64(),
1124 );
1125 if lnv1_result.is_ok() {
1126 return "lnv1";
1127 }
1128
1129 let is_federation_scid = match payment_request.short_channel_id {
1134 Some(scid) => self
1135 .federation_manager
1136 .read()
1137 .await
1138 .get_client_for_index(scid)
1139 .is_some(),
1140 None => false,
1141 };
1142
1143 if is_federation_scid {
1144 Self::cancel_unmatched_lightning_payment(payment_request, lightning_context).await;
1145 "cancel"
1146 } else {
1147 Self::forward_lightning_payment(payment_request, lightning_context).await;
1148 "forward"
1149 }
1150 }
1151
1152 async fn try_handle_lightning_payment_lnv2(
1155 &self,
1156 htlc_request: &InterceptPaymentRequest,
1157 lightning_context: &LightningContext,
1158 ) -> Result<()> {
1159 let (contract, client) = self
1165 .get_registered_incoming_contract_and_client_v2(
1166 PaymentImage::Hash(htlc_request.payment_hash),
1167 htlc_request.amount_msat,
1168 )
1169 .await?;
1170
1171 if let Err(err) = client
1172 .get_first_module::<GatewayClientModuleV2>()
1173 .expect("Must have client module")
1174 .relay_incoming_htlc(
1175 htlc_request.payment_hash,
1176 htlc_request.incoming_chan_id,
1177 htlc_request.htlc_id,
1178 contract,
1179 htlc_request.amount_msat,
1180 )
1181 .await
1182 {
1183 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error relaying incoming lightning payment");
1184
1185 let outcome = InterceptPaymentResponse {
1186 action: PaymentAction::Cancel,
1187 payment_hash: htlc_request.payment_hash,
1188 incoming_chan_id: htlc_request.incoming_chan_id,
1189 htlc_id: htlc_request.htlc_id,
1190 };
1191
1192 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1193 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
1194 }
1195 }
1196
1197 Ok(())
1198 }
1199
1200 async fn try_handle_lightning_payment_ln_legacy(
1203 &self,
1204 htlc_request: &InterceptPaymentRequest,
1205 lightning_context: &LightningContext,
1206 ) -> Result<()> {
1207 let Some(federation_index) = htlc_request.short_channel_id else {
1209 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1210 "Incoming payment has not last hop short channel id".to_string(),
1211 )));
1212 };
1213
1214 let Some(client) = self
1215 .federation_manager
1216 .read()
1217 .await
1218 .get_client_for_index(federation_index)
1219 else {
1220 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())));
1221 };
1222
1223 client
1228 .borrow()
1229 .with(|client| async {
1230 let htlc = htlc_request.clone().try_into();
1231 match htlc {
1232 Ok(htlc) => {
1233 let lnv1 =
1234 client
1235 .get_first_module::<GatewayClientModule>()
1236 .map_err(|_| {
1237 PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1238 "Federation does not have LNv1 module".to_string(),
1239 ))
1240 })?;
1241 match lnv1
1242 .gateway_handle_intercepted_htlc(htlc, async {
1243 Ok(lightning_context.lnrpc.info().await?.block_height)
1244 })
1245 .await
1246 {
1247 Ok(_) => Ok(()),
1248 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1249 format!("Error intercepting lightning payment {e:?}"),
1250 ))),
1251 }
1252 }
1253 _ => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1254 "Could not convert InterceptHtlcResult into an HTLC".to_string(),
1255 ))),
1256 }
1257 })
1258 .await
1259 }
1260
1261 async fn cancel_unmatched_lightning_payment(
1275 htlc_request: InterceptPaymentRequest,
1276 lightning_context: &LightningContext,
1277 ) {
1278 let outcome = InterceptPaymentResponse {
1279 action: PaymentAction::Cancel,
1280 payment_hash: htlc_request.payment_hash,
1281 incoming_chan_id: htlc_request.incoming_chan_id,
1282 htlc_id: htlc_request.htlc_id,
1283 };
1284
1285 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1286 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1287 }
1288 }
1289
1290 async fn forward_lightning_payment(
1295 htlc_request: InterceptPaymentRequest,
1296 lightning_context: &LightningContext,
1297 ) {
1298 let outcome = InterceptPaymentResponse {
1299 action: PaymentAction::Forward,
1300 payment_hash: htlc_request.payment_hash,
1301 incoming_chan_id: htlc_request.incoming_chan_id,
1302 htlc_id: htlc_request.htlc_id,
1303 };
1304
1305 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1306 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1307 }
1308 }
1309
1310 async fn set_gateway_state(&self, state: GatewayState) {
1312 let mut lock = self.state.write().await;
1313 *lock = state;
1314 }
1315
1316 #[doc(hidden)]
1324 pub async fn set_gateway_state_out_of_band(&self, state: GatewayState) {
1325 self.set_gateway_state(state).await;
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 match mint.await_final_receive_operation_state(operation_id).await {
1585 fedimint_mintv2_client::FinalReceiveOperationState::Success => {}
1586 fedimint_mintv2_client::FinalReceiveOperationState::Rejected => {
1587 return Err(PublicGatewayError::ReceiveEcashError {
1588 failure_reason: "ECash receive was rejected".to_string(),
1589 });
1590 }
1591 }
1592 }
1593
1594 Ok(ReceiveEcashResponse { amount })
1595 } else {
1596 Err(PublicGatewayError::ReceiveEcashError {
1597 failure_reason: "No mint module found".to_string(),
1598 })
1599 }
1600 }
1601
1602 pub async fn handle_get_invoice_msg(
1605 &self,
1606 payload: GetInvoiceRequest,
1607 ) -> AdminResult<Option<GetInvoiceResponse>> {
1608 let lightning_context = self.get_lightning_context().await?;
1609 let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1610 Ok(invoice)
1611 }
1612
1613 pub async fn handle_withdraw_to_onchain_msg(
1616 &self,
1617 payload: WithdrawToOnchainPayload,
1618 ) -> AdminResult<WithdrawResponse> {
1619 let address = self.handle_get_ln_onchain_address_msg().await?;
1620 let withdraw = WithdrawPayload {
1621 address: address.into_unchecked(),
1622 federation_id: payload.federation_id,
1623 amount: payload.amount,
1624 quoted_fees: None,
1625 };
1626 self.handle_withdraw_msg(withdraw).await
1627 }
1628
1629 pub async fn handle_pegin_from_onchain_msg(
1632 &self,
1633 payload: PeginFromOnchainPayload,
1634 ) -> AdminResult<Txid> {
1635 let deposit = DepositAddressPayload {
1636 federation_id: payload.federation_id,
1637 };
1638 let address = self.handle_address_msg(deposit).await?;
1639 let send_onchain = SendOnchainRequest {
1640 address: address.into_unchecked(),
1641 amount: payload.amount,
1642 fee_rate_sats_per_vbyte: payload.fee_rate_sats_per_vbyte,
1643 };
1644 let txid = self.handle_send_onchain_msg(send_onchain).await?;
1645
1646 Ok(txid)
1647 }
1648
1649 async fn register_federations(
1651 &self,
1652 federations: &BTreeMap<FederationId, FederationConfig>,
1653 register_task_group: &TaskGroup,
1654 ) {
1655 if let Ok(lightning_context) = self.get_lightning_context().await {
1656 let route_hints = lightning_context
1657 .lnrpc
1658 .parsed_route_hints(self.num_route_hints)
1659 .await;
1660 if route_hints.is_empty() {
1661 warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
1662 }
1663
1664 for (federation_id, federation_config) in federations {
1665 let fed_manager = self.federation_manager.read().await;
1666 if let Some(client) = fed_manager.client(federation_id) {
1667 let client_arc = client.clone().into_value();
1668 let route_hints = route_hints.clone();
1669 let lightning_context = lightning_context.clone();
1670 let federation_config = federation_config.clone();
1671 let registrations =
1672 self.registrations.clone().into_values().collect::<Vec<_>>();
1673
1674 register_task_group.spawn_cancellable_silent(
1675 "register federation",
1676 async move {
1677 let Ok(gateway_client) =
1678 client_arc.get_first_module::<GatewayClientModule>()
1679 else {
1680 return;
1681 };
1682
1683 for registration in registrations {
1684 gateway_client
1685 .try_register_with_federation(
1686 route_hints.clone(),
1687 GW_ANNOUNCEMENT_TTL,
1688 federation_config.lightning_fee.into(),
1689 lightning_context.clone(),
1690 registration.endpoint_url,
1691 registration.keypair,
1692 )
1693 .await;
1694 }
1695 },
1696 );
1697 }
1698 }
1699 }
1700 }
1701
1702 pub async fn select_client(
1705 &self,
1706 federation_id: FederationId,
1707 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
1708 {
1709 self.federation_manager
1710 .read()
1711 .await
1712 .client(&federation_id)
1713 .cloned()
1714 .ok_or(FederationNotConnected {
1715 federation_id_prefix: federation_id.to_prefix(),
1716 })
1717 }
1718
1719 async fn load_mnemonic(gateway_db: &Database) -> Option<Mnemonic> {
1720 let secret = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db)
1721 .await
1722 .ok()?;
1723 Mnemonic::from_entropy(&secret).ok()
1724 }
1725
1726 async fn load_clients(&self) -> AdminResult<()> {
1730 if let GatewayState::NotConfigured { .. } = self.get_state().await {
1731 return Ok(());
1732 }
1733
1734 let mut federation_manager = self.federation_manager.write().await;
1735
1736 let configs = {
1737 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1738 dbtx.load_federation_configs().await
1739 };
1740
1741 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
1742 federation_manager.set_next_index(max_federation_index + 1);
1743 }
1744
1745 let mnemonic = Self::load_mnemonic(&self.gateway_db)
1746 .await
1747 .expect("mnemonic should be set");
1748
1749 for (federation_id, config) in configs {
1750 let federation_index = config.federation_index;
1751 match Box::pin(Spanned::try_new(
1752 info_span!(target: LOG_GATEWAY, "client", federation_id = %federation_id.clone()),
1753 self.client_builder
1754 .build(config, Arc::new(self.clone()), &mnemonic),
1755 ))
1756 .await
1757 {
1758 Ok(client) => {
1759 federation_manager.add_client(federation_index, client);
1760 }
1761 _ => {
1762 warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
1763 }
1764 }
1765 }
1766
1767 Ok(())
1768 }
1769
1770 fn register_clients_timer(&self) {
1776 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1778 info!(target: LOG_GATEWAY, "Spawning register task...");
1779 let gateway = self.clone();
1780 let register_task_group = self.task_group.make_subgroup();
1781 self.task_group.spawn_cancellable("register clients", async move {
1782 loop {
1783 let gateway_state = gateway.get_state().await;
1784 if let GatewayState::Running { .. } = &gateway_state {
1785 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
1786 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
1787 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
1788 } else {
1789 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
1791 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");
1792 sleep(NOT_RUNNING_RETRY).await;
1793 continue;
1794 }
1795
1796 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
1799 }
1800 });
1801 }
1802 }
1803
1804 async fn check_federation_network(
1807 client: &ClientHandleArc,
1808 network: Network,
1809 ) -> AdminResult<()> {
1810 let federation_id = client.federation_id();
1811 let config = client.config().await;
1812
1813 let lnv1_cfg = config
1814 .modules
1815 .values()
1816 .find(|m| LightningCommonInit::KIND == m.kind);
1817
1818 let lnv2_cfg = config
1819 .modules
1820 .values()
1821 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
1822
1823 if lnv1_cfg.is_none() && lnv2_cfg.is_none() {
1825 return Err(AdminGatewayError::ClientCreationError(anyhow!(
1826 "Federation {federation_id} does not have any lightning module (LNv1 or LNv2)"
1827 )));
1828 }
1829
1830 if let Some(cfg) = lnv1_cfg {
1832 let ln_cfg: &LightningClientConfig = cfg.cast()?;
1833
1834 if ln_cfg.network.0 != network {
1835 crit!(
1836 target: LOG_GATEWAY,
1837 federation_id = %federation_id,
1838 network = %network,
1839 "Incorrect LNv1 network for federation",
1840 );
1841 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1842 "Unsupported LNv1 network {}",
1843 ln_cfg.network
1844 ))));
1845 }
1846 }
1847
1848 if let Some(cfg) = lnv2_cfg {
1850 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig = cfg.cast()?;
1851
1852 if ln_cfg.network != network {
1853 crit!(
1854 target: LOG_GATEWAY,
1855 federation_id = %federation_id,
1856 network = %network,
1857 "Incorrect LNv2 network for federation",
1858 );
1859 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1860 "Unsupported LNv2 network {}",
1861 ln_cfg.network
1862 ))));
1863 }
1864 }
1865
1866 Ok(())
1867 }
1868
1869 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 async fn await_lightning_context(&self) -> LightningContext {
1909 loop {
1910 match self.get_lightning_context().await {
1911 Ok(lightning_context) => return lightning_context,
1912 Err(err) => {
1913 let state = self.get_state().await;
1914
1915 warn!(
1916 target: LOG_GATEWAY,
1917 err = %err.fmt_compact(),
1918 %state,
1919 retry_interval_secs = LIGHTNING_CONTEXT_RETRY_INTERVAL.as_secs(),
1920 "Not connected to the lightning node, waiting before asking it again",
1921 );
1922
1923 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
1924 }
1925 }
1926 }
1927 }
1928
1929 pub async fn unannounce_from_all_federations(&self) {
1932 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1933 for registration in self.registrations.values() {
1934 self.federation_manager
1935 .read()
1936 .await
1937 .unannounce_from_all_federations(registration.keypair)
1938 .await;
1939 }
1940 }
1941 }
1942
1943 async fn create_lightning_client(
1944 &self,
1945 runtime: Arc<tokio::runtime::Runtime>,
1946 ) -> Box<dyn ILnRpcClient> {
1947 match self.lightning_mode.clone() {
1948 LightningMode::Lnd {
1949 lnd_rpc_addr,
1950 lnd_tls_cert,
1951 lnd_macaroon,
1952 } => {
1953 let gateway_db = self.gateway_db.clone();
1958 let lnv2_filter: Lnv2HoldInvoiceFilter = Arc::new(move |hash| {
1959 let gateway_db = gateway_db.clone();
1960 Box::pin(async move {
1961 gateway_db
1962 .begin_transaction_nc()
1963 .await
1964 .load_registered_incoming_contract(PaymentImage::Hash(hash))
1965 .await
1966 .is_some()
1967 })
1968 });
1969
1970 Box::new(GatewayLndClient::new(
1971 lnd_rpc_addr,
1972 lnd_tls_cert,
1973 lnd_macaroon,
1974 None,
1975 lnv2_filter,
1976 ))
1977 }
1978 LightningMode::Ldk {
1979 lightning_port,
1980 alias,
1981 } => {
1982 let mnemonic = Self::load_mnemonic(&self.gateway_db)
1983 .await
1984 .expect("mnemonic should be set");
1985 retry("create LDK Node", fibonacci_max_one_hour(), || async {
1989 ldk::GatewayLdkClient::new(
1990 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
1991 self.chain_source.clone(),
1992 self.network,
1993 lightning_port,
1994 alias.clone(),
1995 mnemonic.clone(),
1996 runtime.clone(),
1997 )
1998 .map(Box::new)
1999 })
2000 .await
2001 .expect("Could not create LDK Node")
2002 }
2003 }
2004 }
2005}
2006
2007#[async_trait]
2008impl IAdminGateway for Gateway {
2009 type Error = AdminGatewayError;
2010
2011 async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
2014 let GatewayState::Running { lightning_context } = self.get_state().await else {
2015 return Ok(GatewayInfo {
2016 federations: vec![],
2017 federation_fake_scids: None,
2018 version_hash: fedimint_build_code_version_env!().to_string(),
2019 gateway_state: self.state.read().await.to_string(),
2020 lightning_info: LightningInfo::NotConnected,
2021 lightning_mode: self.lightning_mode.clone(),
2022 registrations: self
2023 .registrations
2024 .iter()
2025 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2026 .collect(),
2027 });
2028 };
2029
2030 let dbtx = self.gateway_db.begin_transaction_nc().await;
2031 let federations = self
2032 .federation_manager
2033 .read()
2034 .await
2035 .federation_info_all_federations(dbtx)
2036 .await;
2037
2038 let channels: BTreeMap<u64, FederationId> = federations
2039 .iter()
2040 .map(|federation_info| {
2041 (
2042 federation_info.config.federation_index,
2043 federation_info.federation_id,
2044 )
2045 })
2046 .collect();
2047
2048 let lightning_info = lightning_context.lnrpc.parsed_node_info().await;
2049
2050 Ok(GatewayInfo {
2051 federations,
2052 federation_fake_scids: Some(channels),
2053 version_hash: fedimint_build_code_version_env!().to_string(),
2054 gateway_state: self.state.read().await.to_string(),
2055 lightning_info,
2056 lightning_mode: self.lightning_mode.clone(),
2057 registrations: self
2058 .registrations
2059 .iter()
2060 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2061 .collect(),
2062 })
2063 }
2064
2065 async fn handle_list_channels_msg(
2068 &self,
2069 ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
2070 let context = self.get_lightning_context().await?;
2071 let response = context.lnrpc.list_channels().await?;
2072 Ok(response.channels)
2073 }
2074
2075 async fn handle_payment_summary_msg(
2078 &self,
2079 PaymentSummaryPayload {
2080 start_millis,
2081 end_millis,
2082 }: PaymentSummaryPayload,
2083 ) -> AdminResult<PaymentSummaryResponse> {
2084 let federation_manager = self.federation_manager.read().await;
2085 let fed_configs = federation_manager.get_all_federation_configs().await;
2086 let federation_ids = fed_configs.keys().collect::<Vec<_>>();
2087 let start = UNIX_EPOCH + Duration::from_millis(start_millis);
2088 let end = UNIX_EPOCH + Duration::from_millis(end_millis);
2089
2090 if start > end {
2091 return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
2092 }
2093
2094 let mut outgoing = StructuredPaymentEvents::default();
2095 let mut incoming = StructuredPaymentEvents::default();
2096 for fed_id in federation_ids {
2097 let client = federation_manager
2098 .client(fed_id)
2099 .expect("No client available")
2100 .value();
2101 let all_events = &get_events_for_duration(client, start, end).await;
2102
2103 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
2104 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
2105 outgoing.combine(&mut lnv1_outgoing);
2106 incoming.combine(&mut lnv1_incoming);
2107 outgoing.combine(&mut lnv2_outgoing);
2108 incoming.combine(&mut lnv2_incoming);
2109 }
2110
2111 Ok(PaymentSummaryResponse {
2112 outgoing: PaymentStats::compute(&outgoing),
2113 incoming: PaymentStats::compute(&incoming),
2114 })
2115 }
2116
2117 async fn handle_leave_federation(
2122 &self,
2123 payload: LeaveFedPayload,
2124 ) -> AdminResult<FederationInfo> {
2125 let mut federation_manager = self.federation_manager.write().await;
2128 let mut dbtx = self.gateway_db.begin_transaction().await;
2129
2130 let federation_info = federation_manager
2131 .leave_federation(
2132 payload.federation_id,
2133 &mut dbtx.to_ref_nc(),
2134 self.registrations.values().collect(),
2135 )
2136 .await?;
2137
2138 dbtx.remove_federation_config(payload.federation_id).await;
2139 dbtx.commit_tx().await;
2140 Ok(federation_info)
2141 }
2142
2143 async fn handle_connect_federation(
2148 &self,
2149 payload: ConnectFedPayload,
2150 ) -> AdminResult<FederationInfo> {
2151 let GatewayState::Running { lightning_context } = self.get_state().await else {
2152 return Err(AdminGatewayError::Lightning(
2153 LightningRpcError::FailedToConnect,
2154 ));
2155 };
2156
2157 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
2158 AdminGatewayError::ClientCreationError(anyhow!(format!(
2159 "Invalid federation member string {e:?}"
2160 )))
2161 })?;
2162
2163 let federation_id = invite_code.federation_id();
2164
2165 let mut federation_manager = self.federation_manager.write().await;
2166
2167 if federation_manager.has_federation(federation_id) {
2169 return Err(AdminGatewayError::ClientCreationError(anyhow!(
2170 "Federation has already been registered"
2171 )));
2172 }
2173
2174 let federation_index = federation_manager.pop_next_index()?;
2177
2178 let federation_config = FederationConfig {
2179 invite_code,
2180 federation_index,
2181 lightning_fee: self.default_routing_fees,
2182 transaction_fee: self.default_transaction_fees,
2183 _connector: ConnectorType::Tcp,
2185 };
2186
2187 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2188 .await
2189 .expect("mnemonic should be set");
2190 let recover = payload.recover.unwrap_or(false);
2191 if recover {
2192 self.client_builder
2193 .recover(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2194 .await?;
2195 }
2196
2197 let client = self
2198 .client_builder
2199 .build(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2200 .await?;
2201
2202 if recover {
2203 client.wait_for_all_active_state_machines().await?;
2204 }
2205
2206 let federation_info = FederationInfo {
2209 federation_id,
2210 federation_name: federation_manager.federation_name(&client).await,
2211 balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
2212 warn!(
2213 target: LOG_GATEWAY,
2214 err = %err.fmt_compact_anyhow(),
2215 %federation_id,
2216 "Balance not immediately available after joining/recovering."
2217 );
2218 Amount::default()
2219 }),
2220 config: federation_config.clone(),
2221 last_backup_time: None,
2222 };
2223
2224 Self::check_federation_network(&client, self.network).await?;
2225 if matches!(self.lightning_mode, LightningMode::Lnd { .. })
2226 && let Ok(lnv1) = client.get_first_module::<GatewayClientModule>()
2227 {
2228 for registration in self.registrations.values() {
2229 lnv1.try_register_with_federation(
2230 Vec::new(),
2232 GW_ANNOUNCEMENT_TTL,
2233 federation_config.lightning_fee.into(),
2234 lightning_context.clone(),
2235 registration.endpoint_url.clone(),
2236 registration.keypair,
2237 )
2238 .await;
2239 }
2240 }
2241
2242 federation_manager.add_client(
2244 federation_index,
2245 Spanned::new(
2246 info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
2247 async { client },
2248 )
2249 .await,
2250 );
2251
2252 let mut dbtx = self.gateway_db.begin_transaction().await;
2253 dbtx.save_federation_config(&federation_config).await;
2254 dbtx.save_federation_backup_record(federation_id, None)
2255 .await;
2256 dbtx.commit_tx().await;
2257 debug!(
2258 target: LOG_GATEWAY,
2259 federation_id = %federation_id,
2260 federation_index = %federation_index,
2261 "Federation connected"
2262 );
2263
2264 Ok(federation_info)
2265 }
2266
2267 async fn handle_set_fees_msg(
2270 &self,
2271 SetFeesPayload {
2272 federation_id,
2273 lightning_base,
2274 lightning_parts_per_million,
2275 transaction_base,
2276 transaction_parts_per_million,
2277 }: SetFeesPayload,
2278 ) -> AdminResult<()> {
2279 let mut dbtx = self.gateway_db.begin_transaction().await;
2280 let mut fed_configs = if let Some(fed_id) = federation_id {
2281 dbtx.load_federation_configs()
2282 .await
2283 .into_iter()
2284 .filter(|(id, _)| *id == fed_id)
2285 .collect::<BTreeMap<_, _>>()
2286 } else {
2287 dbtx.load_federation_configs().await
2288 };
2289
2290 let federation_manager = self.federation_manager.read().await;
2291
2292 for (federation_id, config) in &mut fed_configs {
2293 let mut lightning_fee = config.lightning_fee;
2294 if let Some(lightning_base) = lightning_base {
2295 lightning_fee.base = lightning_base;
2296 }
2297
2298 if let Some(lightning_ppm) = lightning_parts_per_million {
2299 lightning_fee.parts_per_million = lightning_ppm;
2300 }
2301
2302 let mut transaction_fee = config.transaction_fee;
2303 if let Some(transaction_base) = transaction_base {
2304 transaction_fee.base = transaction_base;
2305 }
2306
2307 if let Some(transaction_ppm) = transaction_parts_per_million {
2308 transaction_fee.parts_per_million = transaction_ppm;
2309 }
2310
2311 let client =
2312 federation_manager
2313 .client(federation_id)
2314 .ok_or(FederationNotConnected {
2315 federation_id_prefix: federation_id.to_prefix(),
2316 })?;
2317 let client_config = client.value().config().await;
2318 let contains_lnv2 = client_config
2319 .modules
2320 .values()
2321 .any(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
2322
2323 let send_fees = lightning_fee + transaction_fee;
2325 if contains_lnv2 && send_fees.gt(&PaymentFee::SEND_FEE_LIMIT) {
2326 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2327 "Total Send fees exceeded {}",
2328 PaymentFee::SEND_FEE_LIMIT
2329 )));
2330 }
2331
2332 if contains_lnv2 && transaction_fee.gt(&PaymentFee::RECEIVE_FEE_LIMIT) {
2334 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2335 "Transaction fees exceeded RECEIVE LIMIT {}",
2336 PaymentFee::RECEIVE_FEE_LIMIT
2337 )));
2338 }
2339
2340 config.lightning_fee = lightning_fee;
2341 config.transaction_fee = transaction_fee;
2342 dbtx.save_federation_config(config).await;
2343 }
2344
2345 dbtx.commit_tx().await;
2346
2347 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2348 let register_task_group = TaskGroup::new();
2349
2350 self.register_federations(&fed_configs, ®ister_task_group)
2351 .await;
2352 }
2353
2354 Ok(())
2355 }
2356
2357 async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
2361 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2362 .await
2363 .expect("mnemonic should be set");
2364 let words = mnemonic
2365 .words()
2366 .map(std::string::ToString::to_string)
2367 .collect::<Vec<_>>();
2368 let all_federations = self
2369 .federation_manager
2370 .read()
2371 .await
2372 .get_all_federation_configs()
2373 .await
2374 .keys()
2375 .copied()
2376 .collect::<BTreeSet<_>>();
2377 let legacy_federations = self.client_builder.legacy_federations(all_federations);
2378 let mnemonic_response = MnemonicResponse {
2379 mnemonic: words,
2380 legacy_federations,
2381 };
2382 Ok(mnemonic_response)
2383 }
2384
2385 async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
2388 info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
2389 let context = self.get_lightning_context().await?;
2390 let res = context.lnrpc.open_channel(payload).await?;
2391 info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
2392 Txid::from_str(&res.funding_txid).map_err(|e| {
2393 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2394 failure_reason: format!("Received invalid channel funding txid string {e}"),
2395 })
2396 })
2397 }
2398
2399 async fn handle_close_channels_with_peer_msg(
2402 &self,
2403 payload: CloseChannelsWithPeerRequest,
2404 ) -> AdminResult<CloseChannelsWithPeerResponse> {
2405 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Closing lightning channel...");
2406 let context = self.get_lightning_context().await?;
2407 let response = context
2408 .lnrpc
2409 .close_channels_with_peer(payload.clone())
2410 .await?;
2411 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Initiated channel closure");
2412 Ok(response)
2413 }
2414
2415 async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
2418 let dbtx = self.gateway_db.begin_transaction_nc().await;
2419 let federation_infos = self
2420 .federation_manager
2421 .read()
2422 .await
2423 .federation_info_all_federations(dbtx)
2424 .await;
2425
2426 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
2427 .iter()
2428 .map(|federation_info| FederationBalanceInfo {
2429 federation_id: federation_info.federation_id,
2430 ecash_balance_msats: Amount {
2431 msats: federation_info.balance_msat.msats,
2432 },
2433 })
2434 .collect();
2435
2436 let context = self.get_lightning_context().await?;
2437 let lightning_node_balances = context.lnrpc.get_balances().await?;
2438
2439 Ok(GatewayBalances {
2440 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
2441 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
2442 ecash_balances,
2443 inbound_lightning_liquidity_msats: lightning_node_balances
2444 .inbound_lightning_liquidity_msats,
2445 })
2446 }
2447
2448 async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
2450 let context = self.get_lightning_context().await?;
2451 let response = context.lnrpc.send_onchain(payload.clone()).await?;
2452 let txid =
2453 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
2454 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
2455 })?;
2456 info!(onchain_request = %payload, txid = %txid, "Sent onchain transaction");
2457 Ok(txid)
2458 }
2459
2460 async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
2462 let context = self.get_lightning_context().await?;
2463 let response = context.lnrpc.get_ln_onchain_address().await?;
2464
2465 let address = Address::from_str(&response.address).map_err(|e| {
2466 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2467 failure_reason: e.to_string(),
2468 })
2469 })?;
2470
2471 address.require_network(self.network).map_err(|e| {
2472 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2473 failure_reason: e.to_string(),
2474 })
2475 })
2476 }
2477
2478 async fn handle_deposit_address_msg(
2479 &self,
2480 payload: DepositAddressPayload,
2481 ) -> AdminResult<Address> {
2482 self.handle_address_msg(payload).await
2483 }
2484
2485 async fn handle_receive_ecash_msg(
2486 &self,
2487 payload: ReceiveEcashPayload,
2488 ) -> AdminResult<ReceiveEcashResponse> {
2489 Self::handle_receive_ecash_msg(self, payload)
2490 .await
2491 .map_err(|e| AdminGatewayError::Unexpected(anyhow::anyhow!("{}", e)))
2492 }
2493
2494 async fn handle_create_invoice_for_operator_msg(
2497 &self,
2498 payload: CreateInvoiceForOperatorPayload,
2499 ) -> AdminResult<Bolt11Invoice> {
2500 let GatewayState::Running { lightning_context } = self.get_state().await else {
2501 return Err(AdminGatewayError::Lightning(
2502 LightningRpcError::FailedToConnect,
2503 ));
2504 };
2505
2506 Bolt11Invoice::from_str(
2507 &lightning_context
2508 .lnrpc
2509 .create_invoice(CreateInvoiceRequest {
2510 payment_hash: None, amount_msat: payload.amount_msats,
2513 expiry_secs: payload.expiry_secs.unwrap_or(3600),
2514 description: payload.description.map(InvoiceDescription::Direct),
2515 })
2516 .await?
2517 .invoice,
2518 )
2519 .map_err(|e| {
2520 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2521 failure_reason: e.to_string(),
2522 })
2523 })
2524 }
2525
2526 async fn handle_pay_invoice_for_operator_msg(
2529 &self,
2530 payload: PayInvoiceForOperatorPayload,
2531 ) -> AdminResult<Preimage> {
2532 const BASE_FEE: u64 = 50;
2534 const FEE_DENOMINATOR: u64 = 100;
2535 const MAX_DELAY: u64 = 1008;
2536
2537 let GatewayState::Running { lightning_context } = self.get_state().await else {
2538 return Err(AdminGatewayError::Lightning(
2539 LightningRpcError::FailedToConnect,
2540 ));
2541 };
2542
2543 let max_fee = BASE_FEE
2544 + payload
2545 .invoice
2546 .amount_milli_satoshis()
2547 .context("Invoice is missing amount")?
2548 .saturating_div(FEE_DENOMINATOR);
2549
2550 let res = lightning_context
2551 .lnrpc
2552 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
2553 .await?;
2554 Ok(res.preimage)
2555 }
2556
2557 async fn handle_list_transactions_msg(
2559 &self,
2560 payload: ListTransactionsPayload,
2561 ) -> AdminResult<ListTransactionsResponse> {
2562 let lightning_context = self.get_lightning_context().await?;
2563 let response = lightning_context
2564 .lnrpc
2565 .list_transactions(payload.start_secs, payload.end_secs)
2566 .await?;
2567 Ok(response)
2568 }
2569
2570 async fn handle_spend_ecash_msg(
2572 &self,
2573 payload: SpendEcashPayload,
2574 ) -> AdminResult<SpendEcashResponse> {
2575 let client = self
2576 .select_client(payload.federation_id)
2577 .await?
2578 .into_value();
2579
2580 if let Ok(mint_module) = client.get_first_module::<MintClientModule>() {
2581 let notes = mint_module.send_oob_notes(payload.amount, ()).await?;
2582 debug!(target: LOG_GATEWAY, ?notes, "Spend ecash notes");
2583 Ok(SpendEcashResponse {
2584 notes: notes.to_string(),
2585 })
2586 } else if let Ok(mint_module) = client.get_first_module::<MintV2ClientModule>() {
2587 let ecash = mint_module
2588 .send(payload.amount, serde_json::Value::Null)
2589 .await
2590 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
2591
2592 Ok(SpendEcashResponse {
2593 notes: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
2594 })
2595 } else {
2596 Err(AdminGatewayError::Unexpected(anyhow::anyhow!(
2597 "No mint module available"
2598 )))
2599 }
2600 }
2601
2602 async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
2605 let mut state_guard = self.state.write().await;
2607 if let GatewayState::Running { lightning_context } = state_guard.clone() {
2608 *state_guard = GatewayState::ShuttingDown { lightning_context };
2609
2610 self.federation_manager
2611 .read()
2612 .await
2613 .wait_for_incoming_payments()
2614 .await?;
2615 }
2616
2617 let tg = task_group.clone();
2618 tg.spawn("Kill Gateway", |_task_handle| async {
2619 if let Err(err) = task_group.shutdown_join_all(Duration::from_mins(3)).await {
2620 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error shutting down gateway");
2621 }
2622 });
2623 Ok(())
2624 }
2625
2626 fn get_task_group(&self) -> TaskGroup {
2627 self.task_group.clone()
2628 }
2629
2630 async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> AdminResult<WithdrawResponse> {
2633 let WithdrawPayload {
2634 amount,
2635 address,
2636 federation_id,
2637 quoted_fees,
2638 } = payload;
2639
2640 let address_network = get_network_for_address(&address);
2641 let gateway_network = self.network;
2642 let Ok(address) = address.require_network(gateway_network) else {
2643 return Err(AdminGatewayError::WithdrawError {
2644 failure_reason: format!(
2645 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
2646 ),
2647 });
2648 };
2649
2650 let client = self.select_client(federation_id).await?;
2651
2652 if let Ok(wallet_module) = client
2653 .value()
2654 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
2655 {
2656 return withdraw_v2(client.value(), &wallet_module, &address, amount).await;
2657 }
2658
2659 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
2660
2661 let (withdraw_amount, fees) = match quoted_fees {
2664 Some(fees) => {
2666 let amt = match amount {
2667 BitcoinAmountOrAll::Amount(a) => a,
2668 BitcoinAmountOrAll::All => {
2669 return Err(AdminGatewayError::WithdrawError {
2671 failure_reason:
2672 "Cannot use 'all' with quoted fees - amount must be resolved first"
2673 .to_string(),
2674 });
2675 }
2676 };
2677 (amt, fees)
2678 }
2679 None => match amount {
2681 BitcoinAmountOrAll::All => {
2684 let balance = bitcoin::Amount::from_sat(
2685 client
2686 .value()
2687 .get_balance_for_btc()
2688 .await
2689 .map_err(|err| {
2690 AdminGatewayError::Unexpected(anyhow!(
2691 "Balance not available: {}",
2692 err.fmt_compact_anyhow()
2693 ))
2694 })?
2695 .msats
2696 / 1000,
2697 );
2698 let fees = wallet_module.get_withdraw_fees(&address, balance).await?;
2699 let withdraw_amount = balance.checked_sub(fees.amount());
2700 if withdraw_amount.is_none() {
2701 return Err(AdminGatewayError::WithdrawError {
2702 failure_reason: format!(
2703 "Insufficient funds. Balance: {balance} Fees: {fees:?}"
2704 ),
2705 });
2706 }
2707 (withdraw_amount.expect("checked above"), fees)
2708 }
2709 BitcoinAmountOrAll::Amount(amount) => (
2710 amount,
2711 wallet_module.get_withdraw_fees(&address, amount).await?,
2712 ),
2713 },
2714 };
2715
2716 let operation_id = wallet_module
2717 .withdraw(&address, withdraw_amount, fees, ())
2718 .await?;
2719 let mut updates = wallet_module
2720 .subscribe_withdraw_updates(operation_id)
2721 .await?
2722 .into_stream();
2723
2724 while let Some(update) = updates.next().await {
2725 match update {
2726 WithdrawState::Succeeded(txid) => {
2727 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds");
2728 return Ok(WithdrawResponse { txid, fees });
2729 }
2730 WithdrawState::Failed(e) => {
2731 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
2732 }
2733 WithdrawState::Created => {}
2734 }
2735 }
2736
2737 Err(AdminGatewayError::WithdrawError {
2738 failure_reason: "Ran out of state updates while withdrawing".to_string(),
2739 })
2740 }
2741
2742 async fn handle_withdraw_preview_msg(
2745 &self,
2746 payload: WithdrawPreviewPayload,
2747 ) -> AdminResult<WithdrawPreviewResponse> {
2748 let gateway_network = self.network;
2749 let address_checked = payload
2750 .address
2751 .clone()
2752 .require_network(gateway_network)
2753 .map_err(|_| AdminGatewayError::WithdrawError {
2754 failure_reason: "Address network mismatch".to_string(),
2755 })?;
2756
2757 let client = self.select_client(payload.federation_id).await?;
2758
2759 let WithdrawDetails {
2760 amount,
2761 mint_fees,
2762 peg_out_fees,
2763 } = match payload.amount {
2764 BitcoinAmountOrAll::All => {
2765 calculate_max_withdrawable(client.value(), &address_checked).await?
2766 }
2767 BitcoinAmountOrAll::Amount(btc_amount) => {
2768 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
2769 WithdrawDetails {
2770 amount: btc_amount.into(),
2771 mint_fees: None,
2772 peg_out_fees: wallet_module
2773 .get_withdraw_fees(&address_checked, btc_amount)
2774 .await?,
2775 }
2776 } else if let Ok(wallet_module) = client
2777 .value()
2778 .get_first_module::<fedimint_walletv2_client::WalletClientModule>(
2779 ) {
2780 let fee = wallet_module.send_fee().await.map_err(|e| {
2781 AdminGatewayError::WithdrawError {
2782 failure_reason: e.to_string(),
2783 }
2784 })?;
2785 WithdrawDetails {
2786 amount: btc_amount.into(),
2787 mint_fees: None,
2788 peg_out_fees: PegOutFees::from_amount(fee),
2789 }
2790 } else {
2791 return Err(AdminGatewayError::Unexpected(anyhow!(
2792 "No wallet module found"
2793 )));
2794 }
2795 }
2796 };
2797
2798 let total_cost = amount
2799 .checked_add(peg_out_fees.amount().into())
2800 .and_then(|a| a.checked_add(mint_fees.unwrap_or(Amount::ZERO)))
2801 .ok_or_else(|| AdminGatewayError::Unexpected(anyhow!("Total cost overflow")))?;
2802
2803 Ok(WithdrawPreviewResponse {
2804 withdraw_amount: amount,
2805 address: payload.address.assume_checked().to_string(),
2806 peg_out_fees,
2807 total_cost,
2808 mint_fees,
2809 })
2810 }
2811
2812 async fn handle_payment_log_msg(
2814 &self,
2815 PaymentLogPayload {
2816 end_position,
2817 pagination_size,
2818 federation_id,
2819 event_kinds,
2820 }: PaymentLogPayload,
2821 ) -> AdminResult<PaymentLogResponse> {
2822 const BATCH_SIZE: u64 = 10_000;
2823 let federation_manager = self.federation_manager.read().await;
2824 let client = federation_manager
2825 .client(&federation_id)
2826 .ok_or(FederationNotConnected {
2827 federation_id_prefix: federation_id.to_prefix(),
2828 })?
2829 .value();
2830
2831 let event_kinds = if event_kinds.is_empty() {
2832 ALL_GATEWAY_EVENTS.to_vec()
2833 } else {
2834 event_kinds
2835 };
2836
2837 let end_position = if let Some(position) = end_position {
2838 position
2839 } else {
2840 let mut dbtx = client.db().begin_transaction_nc().await;
2841 dbtx.get_next_event_log_id().await
2842 };
2843
2844 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
2845
2846 let mut payment_log = Vec::new();
2847
2848 while payment_log.len() < pagination_size {
2849 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
2850 let mut filtered_batch = batch
2851 .into_iter()
2852 .filter(|e| e.id() <= end_position && event_kinds.contains(&e.as_raw().kind))
2853 .collect::<Vec<_>>();
2854 filtered_batch.reverse();
2855 payment_log.extend(filtered_batch);
2856
2857 start_position = start_position.saturating_sub(BATCH_SIZE);
2859
2860 if start_position == EventLogId::LOG_START {
2861 break;
2862 }
2863 }
2864
2865 payment_log.truncate(pagination_size);
2867
2868 Ok(PaymentLogResponse(payment_log))
2869 }
2870
2871 async fn handle_set_mnemonic_msg(&self, payload: SetMnemonicPayload) -> AdminResult<()> {
2874 let GatewayState::NotConfigured { mnemonic_sender } = self.get_state().await else {
2876 return Err(AdminGatewayError::MnemonicError(anyhow!(
2877 "Gateway is not is NotConfigured state"
2878 )));
2879 };
2880
2881 let mnemonic = if let Some(words) = payload.words {
2882 info!(target: LOG_GATEWAY, "Using user provided mnemonic");
2883 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(|e| {
2884 AdminGatewayError::MnemonicError(anyhow!(format!(
2885 "Seed phrase provided in environment was invalid {e:?}"
2886 )))
2887 })?
2888 } else {
2889 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
2890 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
2891 };
2892
2893 Client::store_encodable_client_secret(&self.gateway_db, mnemonic.to_entropy())
2894 .await
2895 .map_err(AdminGatewayError::MnemonicError)?;
2896
2897 self.set_gateway_state(GatewayState::Disconnected).await;
2898
2899 let _ = mnemonic_sender.send(());
2901
2902 Ok(())
2903 }
2904
2905 async fn handle_create_offer_for_operator_msg(
2907 &self,
2908 payload: CreateOfferPayload,
2909 ) -> AdminResult<CreateOfferResponse> {
2910 let lightning_context = self.get_lightning_context().await?;
2911 let offer = lightning_context.lnrpc.create_offer(
2912 payload.amount,
2913 payload.description,
2914 payload.expiry_secs,
2915 payload.quantity,
2916 )?;
2917 Ok(CreateOfferResponse { offer })
2918 }
2919
2920 async fn handle_pay_offer_for_operator_msg(
2922 &self,
2923 payload: PayOfferPayload,
2924 ) -> AdminResult<PayOfferResponse> {
2925 let lightning_context = self.get_lightning_context().await?;
2926 let preimage = lightning_context
2927 .lnrpc
2928 .pay_offer(
2929 payload.offer,
2930 payload.quantity,
2931 payload.amount,
2932 payload.payer_note,
2933 )
2934 .await?;
2935 Ok(PayOfferResponse {
2936 preimage: preimage.to_string(),
2937 })
2938 }
2939
2940 async fn handle_export_invite_codes(
2943 &self,
2944 ) -> BTreeMap<FederationId, BTreeMap<PeerId, (String, InviteCode)>> {
2945 let fed_manager = self.federation_manager.read().await;
2946 fed_manager.all_invite_codes().await
2947 }
2948
2949 async fn handle_get_note_summary_msg(
2952 &self,
2953 federation_id: &FederationId,
2954 ) -> AdminResult<TieredCounts> {
2955 let fed_manager = self.federation_manager.read().await;
2956 fed_manager.get_note_summary(federation_id).await
2957 }
2958
2959 fn get_password_hash(&self) -> String {
2960 self.bcrypt_password_hash.clone()
2961 }
2962
2963 fn gatewayd_version(&self) -> String {
2964 let gatewayd_version = env!("CARGO_PKG_VERSION");
2965 gatewayd_version.to_string()
2966 }
2967
2968 async fn get_chain_source(&self) -> (ChainSource, Network) {
2969 (self.chain_source.clone(), self.network)
2970 }
2971
2972 fn lightning_mode(&self) -> LightningMode {
2973 self.lightning_mode.clone()
2974 }
2975
2976 async fn is_configured(&self) -> bool {
2977 !matches!(self.get_state().await, GatewayState::NotConfigured { .. })
2978 }
2979}
2980
2981impl Gateway {
2983 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
2987 self.federation_manager
2988 .read()
2989 .await
2990 .client(federation_id)
2991 .and_then(|client| {
2992 client
2995 .value()
2996 .get_first_module::<GatewayClientModuleV2>()
2997 .ok()
2998 .map(|module| module.keypair.public_key())
2999 })
3000 }
3001
3002 pub async fn routing_info_v2(
3005 &self,
3006 federation_id: &FederationId,
3007 ) -> Result<Option<RoutingInfo>> {
3008 let context = self.get_lightning_context().await?;
3009
3010 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
3011 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
3012 PublicGatewayError::FederationNotConnected(FederationNotConnected {
3013 federation_id_prefix: federation_id.to_prefix(),
3014 }),
3015 )?;
3016
3017 let lightning_fee = fed_config.lightning_fee;
3018 let transaction_fee = fed_config.transaction_fee;
3019
3020 Ok(self
3021 .public_key_v2(federation_id)
3022 .await
3023 .map(|module_public_key| RoutingInfo {
3024 lightning_public_key: context.lightning_public_key,
3025 lightning_alias: Some(context.lightning_alias.clone()),
3026 module_public_key,
3027 send_fee_default: lightning_fee + transaction_fee,
3028 send_fee_minimum: transaction_fee,
3032 expiration_delta_default: 1440,
3033 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
3034 receive_fee: transaction_fee,
3037 }))
3038 }
3039
3040 pub async fn send_payment_v2(
3043 &self,
3044 payload: SendPaymentPayload,
3045 ) -> Result<std::result::Result<[u8; 32], Signature>> {
3046 let client = self.select_client(payload.federation_id).await?;
3047 let module = client
3050 .value()
3051 .get_first_module::<GatewayClientModuleV2>()
3052 .map_err(|err| PublicGatewayError::LNv2(LNv2Error::OutgoingPayment(err)))?;
3053
3054 module
3055 .send_payment(payload)
3056 .await
3057 .map_err(LNv2Error::OutgoingPayment)
3058 .map_err(PublicGatewayError::LNv2)
3059 }
3060
3061 async fn create_bolt11_invoice_v2(
3066 &self,
3067 payload: CreateBolt11InvoicePayload,
3068 ) -> Result<Bolt11Invoice> {
3069 if !payload.contract.verify() {
3070 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3071 "The contract is invalid".to_string(),
3072 )));
3073 }
3074
3075 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
3076 LNv2Error::IncomingPayment(format!(
3077 "Federation {} does not exist",
3078 payload.federation_id
3079 )),
3080 )?;
3081
3082 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
3083 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3084 "The incoming contract is keyed to another gateway".to_string(),
3085 )));
3086 }
3087
3088 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
3089
3090 if contract_amount == Amount::ZERO {
3091 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3092 "Zero amount incoming contracts are not supported".to_string(),
3093 )));
3094 }
3095
3096 if contract_amount != payload.contract.commitment.amount {
3097 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3098 "The contract amount does not pay the correct amount of fees".to_string(),
3099 )));
3100 }
3101
3102 if payload.contract.commitment.expiration <= duration_since_epoch().as_secs() {
3103 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3104 "The contract has already expired".to_string(),
3105 )));
3106 }
3107
3108 let payment_hash = match payload.contract.commitment.payment_image {
3109 PaymentImage::Hash(payment_hash) => payment_hash,
3110 PaymentImage::Point(..) => {
3111 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3112 "PaymentImage is not a payment hash".to_string(),
3113 )));
3114 }
3115 };
3116
3117 let invoice = self
3118 .create_invoice_via_lnrpc_v2(
3119 payment_hash,
3120 payload.amount,
3121 payload.description.clone(),
3122 payload.expiry_secs,
3123 )
3124 .await?;
3125
3126 let mut dbtx = self.gateway_db.begin_transaction().await;
3127
3128 if dbtx
3129 .save_registered_incoming_contract(
3130 payload.federation_id,
3131 payload.amount,
3132 payload.contract,
3133 )
3134 .await
3135 .is_some()
3136 {
3137 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3138 "PaymentHash is already registered".to_string(),
3139 )));
3140 }
3141
3142 dbtx.commit_tx_result().await.map_err(|_| {
3143 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3144 "Payment hash is already registered".to_string(),
3145 ))
3146 })?;
3147
3148 Ok(invoice)
3149 }
3150
3151 pub async fn create_invoice_via_lnrpc_v2(
3154 &self,
3155 payment_hash: sha256::Hash,
3156 amount: Amount,
3157 description: Bolt11InvoiceDescription,
3158 expiry_time: u32,
3159 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
3160 let lnrpc = self.get_lightning_context().await?.lnrpc;
3161
3162 let response = match description {
3163 Bolt11InvoiceDescription::Direct(description) => {
3164 lnrpc
3165 .create_invoice(CreateInvoiceRequest {
3166 payment_hash: Some(payment_hash),
3167 amount_msat: amount.msats,
3168 expiry_secs: expiry_time,
3169 description: Some(InvoiceDescription::Direct(description)),
3170 })
3171 .await?
3172 }
3173 Bolt11InvoiceDescription::Hash(hash) => {
3174 lnrpc
3175 .create_invoice(CreateInvoiceRequest {
3176 payment_hash: Some(payment_hash),
3177 amount_msat: amount.msats,
3178 expiry_secs: expiry_time,
3179 description: Some(InvoiceDescription::Hash(hash)),
3180 })
3181 .await?
3182 }
3183 };
3184
3185 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
3186 LightningRpcError::FailedToGetInvoice {
3187 failure_reason: e.to_string(),
3188 }
3189 })
3190 }
3191
3192 pub async fn verify_bolt11_preimage_v2(
3193 &self,
3194 payment_hash: sha256::Hash,
3195 wait: bool,
3196 ) -> std::result::Result<VerifyResponse, String> {
3197 let registered_contract = self
3198 .gateway_db
3199 .begin_transaction_nc()
3200 .await
3201 .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3202 .await
3203 .ok_or("Unknown payment hash".to_string())?;
3204
3205 let client = self
3206 .select_client(registered_contract.federation_id)
3207 .await
3208 .map_err(|_| "Not connected to federation".to_string())?
3209 .into_value();
3210
3211 let operation_id = OperationId::from_encodable(®istered_contract.contract);
3212
3213 if !(wait || client.operation_exists(operation_id).await) {
3214 return Ok(VerifyResponse {
3215 settled: false,
3216 preimage: None,
3217 });
3218 }
3219
3220 let state = client
3221 .get_first_module::<GatewayClientModuleV2>()
3222 .expect("Must have client module")
3223 .await_receive(operation_id)
3224 .await;
3225
3226 let preimage = match state {
3227 FinalReceiveState::Success(preimage) => Ok(preimage),
3228 FinalReceiveState::Failure => Err("Payment has failed".to_string()),
3229 FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
3230 FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
3231 }?;
3232
3233 Ok(VerifyResponse {
3234 settled: true,
3235 preimage: Some(preimage),
3236 })
3237 }
3238
3239 pub async fn get_registered_incoming_contract_and_client_v2(
3243 &self,
3244 payment_image: PaymentImage,
3245 amount_msats: u64,
3246 ) -> Result<(IncomingContract, ClientHandleArc)> {
3247 let registered_incoming_contract = self
3248 .gateway_db
3249 .begin_transaction_nc()
3250 .await
3251 .load_registered_incoming_contract(payment_image)
3252 .await
3253 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3254 "No corresponding decryption contract available".to_string(),
3255 )))?;
3256
3257 if registered_incoming_contract.incoming_amount_msats != amount_msats {
3258 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3259 "The available decryption contract's amount is not equal to the requested amount"
3260 .to_string(),
3261 )));
3262 }
3263
3264 let client = self
3265 .select_client(registered_incoming_contract.federation_id)
3266 .await?
3267 .into_value();
3268
3269 Ok((registered_incoming_contract.contract, client))
3270 }
3271}
3272
3273#[async_trait]
3274impl IGatewayClientV2 for Gateway {
3275 async fn complete_htlc(
3276 &self,
3277 htlc_response: InterceptPaymentResponse,
3278 ) -> std::result::Result<(), LightningRpcError> {
3279 loop {
3280 let lightning_context = self.await_lightning_context().await;
3281
3282 match lightning_context
3283 .lnrpc
3284 .complete_htlc(htlc_response.clone())
3285 .await
3286 {
3287 Ok(..) => return Ok(()),
3288 Err(err @ LightningRpcError::HtlcCompletionRejected { .. }) => {
3289 warn!(
3290 target: LOG_GATEWAY,
3291 err = %err.fmt_compact(),
3292 "Lightning cannot reach the requested terminal HTLC outcome",
3293 );
3294 return Err(err);
3295 }
3296 Err(err) => {
3297 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
3298 }
3299 }
3300
3301 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
3302 }
3303 }
3304
3305 async fn is_direct_swap(
3306 &self,
3307 invoice: &Bolt11Invoice,
3308 ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>> {
3309 let lightning_context = self.await_lightning_context().await;
3314 if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
3315 let (contract, client) = self
3316 .get_registered_incoming_contract_and_client_v2(
3317 PaymentImage::Hash(*invoice.payment_hash()),
3318 invoice
3319 .amount_milli_satoshis()
3320 .expect("The amount invoice has been previously checked"),
3321 )
3322 .await?;
3323 Ok(Some((contract, client)))
3324 } else {
3325 Ok(None)
3326 }
3327 }
3328
3329 async fn pay(
3330 &self,
3331 invoice: Bolt11Invoice,
3332 max_delay: u64,
3333 max_fee: Amount,
3334 ) -> std::result::Result<[u8; 32], LightningRpcError> {
3335 let lightning_context = self.await_lightning_context().await;
3338 lightning_context
3339 .lnrpc
3340 .pay(invoice, max_delay, max_fee)
3341 .await
3342 .map(|response| response.preimage.0)
3343 }
3344
3345 async fn min_contract_amount(
3346 &self,
3347 federation_id: &FederationId,
3348 amount: u64,
3349 ) -> anyhow::Result<Amount> {
3350 Ok(self
3351 .routing_info_v2(federation_id)
3352 .await?
3353 .ok_or(anyhow!("Routing Info not available"))?
3354 .send_fee_minimum
3355 .add_to(amount))
3356 }
3357
3358 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
3359 let rhints = invoice.route_hints();
3360 let hop = rhints.first().and_then(|rh| rh.0.last())?;
3361
3362 let lightning_context = self.await_lightning_context().await;
3365 if hop.src_node_id != lightning_context.lightning_public_key {
3366 return None;
3367 }
3368
3369 self.federation_manager
3370 .read()
3371 .await
3372 .get_client_for_index(hop.short_channel_id)
3373 }
3374
3375 async fn relay_lnv1_swap(
3376 &self,
3377 client: &ClientHandleArc,
3378 invoice: &Bolt11Invoice,
3379 ) -> anyhow::Result<FinalReceiveState> {
3380 let swap_params = SwapParameters {
3381 payment_hash: *invoice.payment_hash(),
3382 amount_msat: Amount::from_msats(
3383 invoice
3384 .amount_milli_satoshis()
3385 .ok_or(anyhow!("Amountless invoice not supported"))?,
3386 ),
3387 };
3388 let lnv1 = client
3389 .get_first_module::<GatewayClientModule>()
3390 .expect("No LNv1 module");
3391 let operation_id = lnv1.gateway_handle_direct_swap(swap_params).await?;
3392 let mut stream = lnv1
3393 .gateway_subscribe_ln_receive(operation_id)
3394 .await?
3395 .into_stream();
3396 let mut final_state = FinalReceiveState::Failure;
3397 while let Some(update) = stream.next().await {
3398 match update {
3399 GatewayExtReceiveStates::Funding => {}
3400 GatewayExtReceiveStates::FundingFailed { error: _ } => {
3401 final_state = FinalReceiveState::Rejected;
3402 }
3403 GatewayExtReceiveStates::Preimage(preimage) => {
3404 final_state = FinalReceiveState::Success(preimage.0);
3405 }
3406 GatewayExtReceiveStates::RefundError {
3407 error_message: _,
3408 error: _,
3409 } => {
3410 final_state = FinalReceiveState::Failure;
3411 }
3412 GatewayExtReceiveStates::RefundSuccess {
3413 out_points: _,
3414 error: _,
3415 } => {
3416 final_state = FinalReceiveState::Refunded;
3417 }
3418 }
3419 }
3420
3421 Ok(final_state)
3422 }
3423}
3424
3425#[async_trait]
3426impl IGatewayClientV1 for Gateway {
3427 async fn verify_preimage_authentication(
3428 &self,
3429 payment_hash: sha256::Hash,
3430 preimage_auth: sha256::Hash,
3431 contract: OutgoingContractAccount,
3432 ) -> std::result::Result<(), OutgoingPaymentError> {
3433 let mut dbtx = self.gateway_db.begin_transaction().await;
3434 if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
3435 if secret_hash != preimage_auth {
3436 return Err(OutgoingPaymentError {
3437 error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
3438 contract_id: contract.contract.contract_id(),
3439 contract: Some(contract),
3440 });
3441 }
3442 } else {
3443 dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
3446 .await;
3447 return dbtx
3448 .commit_tx_result()
3449 .await
3450 .map_err(|_| OutgoingPaymentError {
3451 error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
3452 contract_id: contract.contract.contract_id(),
3453 contract: Some(contract),
3454 });
3455 }
3456
3457 Ok(())
3458 }
3459
3460 async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()> {
3461 if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
3462 let lightning_context = self.get_lightning_context().await?;
3463
3464 ensure!(
3465 lightning_context.lnrpc.supports_private_payments(),
3466 "Private payments are not supported by the lightning node"
3467 );
3468 }
3469
3470 Ok(())
3471 }
3472
3473 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
3474 let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
3475 gateway_dbtx
3476 .load_federation_config(federation_id)
3477 .await
3478 .map(|c| c.lightning_fee.into())
3479 }
3480
3481 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
3482 self.federation_manager
3483 .read()
3484 .await
3485 .client(federation_id)
3486 .cloned()
3487 }
3488
3489 async fn get_client_for_invoice(
3490 &self,
3491 payment_data: PaymentData,
3492 ) -> Option<Spanned<ClientHandleArc>> {
3493 let rhints = payment_data.route_hints();
3494 let hop = rhints.first().and_then(|rh| rh.0.last())?;
3495
3496 let lightning_context = self.await_lightning_context().await;
3499 if hop.src_node_id != lightning_context.lightning_public_key {
3500 return None;
3501 }
3502
3503 self.federation_manager
3504 .read()
3505 .await
3506 .get_client_for_index(hop.short_channel_id)
3507 }
3508
3509 async fn pay(
3510 &self,
3511 payment_data: PaymentData,
3512 max_delay: u64,
3513 max_fee: Amount,
3514 ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
3515 let lightning_context = self.await_lightning_context().await;
3518
3519 match payment_data {
3520 PaymentData::Invoice(invoice) => {
3521 lightning_context
3522 .lnrpc
3523 .pay(invoice, max_delay, max_fee)
3524 .await
3525 }
3526 PaymentData::PrunedInvoice(invoice) => {
3527 lightning_context
3528 .lnrpc
3529 .pay_private(invoice, max_delay, max_fee)
3530 .await
3531 }
3532 }
3533 }
3534
3535 async fn complete_htlc(
3536 &self,
3537 htlc: InterceptPaymentResponse,
3538 ) -> std::result::Result<(), LightningRpcError> {
3539 let lightning_context = self.await_lightning_context().await;
3541
3542 lightning_context.lnrpc.complete_htlc(htlc).await
3543 }
3544
3545 async fn is_lnv2_direct_swap(
3546 &self,
3547 payment_hash: sha256::Hash,
3548 amount: Amount,
3549 ) -> anyhow::Result<
3550 Option<(
3551 fedimint_lnv2_common::contracts::IncomingContract,
3552 ClientHandleArc,
3553 )>,
3554 > {
3555 let (contract, client) = self
3556 .get_registered_incoming_contract_and_client_v2(
3557 PaymentImage::Hash(payment_hash),
3558 amount.msats,
3559 )
3560 .await?;
3561 Ok(Some((contract, client)))
3562 }
3563}