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 federation_status;
24mod iroh_server;
25mod metrics;
26mod rate_limit;
27mod registration_health;
28pub mod rpc_server;
29mod types;
30
31use std::collections::{BTreeMap, BTreeSet};
32use std::env;
33use std::fmt::Display;
34use std::net::SocketAddr;
35use std::str::FromStr;
36use std::sync::Arc;
37use std::time::{Duration, UNIX_EPOCH};
38
39use anyhow::{Context, anyhow};
40use async_trait::async_trait;
41use bitcoin::hashes::sha256;
42use bitcoin::{Address, Network, Txid, secp256k1};
43use clap::Parser;
44use client::GatewayClientBuilder;
45pub use config::GatewayParameters;
46use config::{DatabaseBackend, GatewayOpts};
47use envs::FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV;
48use error::FederationNotConnected;
49use events::ALL_GATEWAY_EVENTS;
50use federation_manager::FederationManager;
51use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic};
52use fedimint_bitcoind::bitcoincore::BitcoindClient;
53use fedimint_bitcoind::{EsploraClient, IBitcoindRpc};
54use fedimint_client::module_init::ClientModuleInitRegistry;
55use fedimint_client::secret::RootSecretStrategy;
56use fedimint_client::{Client, ClientHandleArc};
57use fedimint_core::base32::{self, FEDIMINT_PREFIX};
58use fedimint_core::config::FederationId;
59use fedimint_core::core::OperationId;
60use fedimint_core::db::{Committable, Database, DatabaseTransaction, apply_migrations};
61use fedimint_core::envs::is_env_var_set;
62use fedimint_core::invite_code::InviteCode;
63use fedimint_core::module::CommonModuleInit;
64use fedimint_core::module::registry::ModuleDecoderRegistry;
65use fedimint_core::rustls::install_crypto_provider;
66use fedimint_core::secp256k1::PublicKey;
67use fedimint_core::secp256k1::schnorr::Signature;
68use fedimint_core::task::{TaskGroup, TaskHandle, TaskShutdownToken, sleep, timeout};
69use fedimint_core::time::duration_since_epoch;
70use fedimint_core::util::backoff_util::fibonacci_max_one_hour;
71use fedimint_core::util::{FmtCompact, SafeUrl, Spanned, retry};
72use fedimint_core::{
73 Amount, BitcoinAmountOrAll, PeerId, TieredCounts, crit, fedimint_build_code_version_env,
74 get_network_for_address,
75};
76use fedimint_eventlog::{DBTransactionEventLogExt, EventLogId, StructuredPaymentEvents};
77use fedimint_gateway_common::{
78 BackupPayload, ChainSource, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse,
79 ConnectFedPayload, ConnectPeerRequest, ConnectorType, CreateInvoiceForOperatorPayload,
80 CreateOfferPayload, CreateOfferResponse, DepositAddressPayload, DepositAddressRecheckPayload,
81 FederationBalanceInfo, FederationConfig, FederationInfo, GatewayBalances, GatewayFedConfig,
82 GatewayInfo, GetInvoiceRequest, GetInvoiceResponse, LeaveFedPayload, LightningInfo,
83 LightningMode, ListTransactionsPayload, ListTransactionsResponse, MnemonicResponse,
84 OpenChannelRequest, PayInvoiceForOperatorPayload, PayOfferPayload, PayOfferResponse,
85 PaymentLogPayload, PaymentLogResponse, PaymentPolicy, PaymentStats, PaymentSummaryPayload,
86 PaymentSummaryResponse, PeginFromOnchainPayload, ReceiveEcashPayload, ReceiveEcashResponse,
87 RegisteredProtocol, SendOnchainRequest, SetChannelFeesRequest, SetFeesPayload,
88 SetMnemonicPayload, SetPaymentPolicyPayload, SpendEcashPayload, SpendEcashResponse,
89 V1_API_ENDPOINT, WithdrawPayload, WithdrawPreviewPayload, WithdrawPreviewResponse,
90 WithdrawResponse, WithdrawToOnchainPayload,
91};
92use fedimint_gateway_server_db::{GatewayDbtxNcExt as _, get_gatewayd_database_migrations};
93pub use fedimint_gateway_ui::IAdminGateway;
94use fedimint_gw_client::events::compute_lnv1_stats;
95use fedimint_gw_client::pay::{OutgoingPaymentError, OutgoingPaymentErrorType};
96use fedimint_gw_client::{
97 GatewayClientModule, GatewayClientV1Error, GatewayExtPayStates, GatewayExtReceiveStates, Htlc,
98 IGatewayClientV1, SwapParameters,
99};
100use fedimint_gwv2_client::events::compute_lnv2_stats;
101use fedimint_gwv2_client::{
102 EXPIRATION_DELTA_MINIMUM_V2, FinalReceiveState, GatewayClientModuleV2, GatewayClientV2Error,
103 IGatewayClientV2,
104};
105use fedimint_lightning::lnd::GatewayLndClient;
106use fedimint_lightning::{
107 CreateInvoiceRequest, ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse,
108 InvoiceDescription, LightningContext, LightningRpcError, LnRpcTracked, Lnv2HoldInvoiceFilter,
109 PayInvoiceResponse, PaymentAction, RouteHtlcStream, ldk,
110};
111use fedimint_ln_client::pay::PaymentData;
112use fedimint_ln_common::config::LightningClientConfig;
113use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
114use fedimint_ln_common::contracts::{IdentifiableContract, Preimage};
115use fedimint_ln_common::{LightningCommonInit, PreimageAuth};
116use fedimint_lnurl::VerifyResponse;
117use fedimint_lnv2_common::Bolt11InvoiceDescription;
118use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
119use fedimint_lnv2_common::gateway_api::{
120 CreateBolt11InvoicePayload, MAX_INVOICE_EXPIRY_SECS, PaymentFee, RoutingInfo,
121 SendPaymentPayload,
122};
123use fedimint_logging::LOG_GATEWAY;
124use fedimint_mint_client::{MintClientInit, MintClientModule, OOBNotes, ReissueExternalNotesState};
125use fedimint_mintv2_client::{
126 MintClientInit as MintV2ClientInit, MintClientModule as MintV2ClientModule,
127};
128use fedimint_wallet_client::{PegOutFees, WalletClientInit, WalletClientModule, WithdrawState};
129use futures::stream::StreamExt;
130use lightning_invoice::{Bolt11Invoice, RoutingFees};
131use rand::rngs::OsRng;
132use tokio::sync::RwLock;
133use tracing::{debug, info, info_span, warn};
134
135use crate::envs::FM_GATEWAY_MNEMONIC_ENV;
136use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError};
137use crate::events::get_events_for_duration;
138use crate::rate_limit::TokenBucketRateLimiter;
139use crate::registration_health::RegistrationHealthTracker;
140use crate::rpc_server::run_webserver;
141use crate::types::PrettyInterceptPaymentRequest;
142
143const GW_ANNOUNCEMENT_TTL: Duration = Duration::from_mins(10);
145
146const DEFAULT_NUM_ROUTE_HINTS: u32 = 1;
149
150const DEFAULT_INVOICE_RATE_LIMIT_BURST: u32 = 50;
152
153const DEFAULT_INVOICE_RATE_LIMIT_PER_SECOND: u32 = 5;
156
157pub const DEFAULT_NETWORK: Network = Network::Regtest;
159
160const LIGHTNING_CONTEXT_RETRY_INTERVAL: Duration = Duration::from_secs(5);
163
164const VERIFY_WAIT_TIMEOUT: Duration = Duration::from_secs(30);
172
173pub type Result<T> = std::result::Result<T, PublicGatewayError>;
174pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
175
176const DB_FILE: &str = "gatewayd.db";
179
180const LDK_NODE_DB_FOLDER: &str = "ldk_node";
183
184#[cfg_attr(doc, aquamarine::aquamarine)]
185#[derive(Clone, Debug)]
198pub enum GatewayState {
199 NotConfigured {
200 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
203 },
204 Disconnected,
205 Syncing,
206 Connected,
207 Running {
208 lightning_context: LightningContext,
209 },
210 ShuttingDown {
211 lightning_context: LightningContext,
212 },
213}
214
215impl Display for GatewayState {
216 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
217 match self {
218 GatewayState::NotConfigured { .. } => write!(f, "NotConfigured"),
219 GatewayState::Disconnected => write!(f, "Disconnected"),
220 GatewayState::Syncing => write!(f, "Syncing"),
221 GatewayState::Connected => write!(f, "Connected"),
222 GatewayState::Running { .. } => write!(f, "Running"),
223 GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"),
224 }
225 }
226}
227
228#[derive(Debug, Clone)]
231struct Registration {
232 endpoint_url: SafeUrl,
234
235 keypair: secp256k1::Keypair,
237}
238
239impl Registration {
240 pub async fn new(db: &Database, endpoint_url: SafeUrl, protocol: RegisteredProtocol) -> Self {
241 let keypair = Gateway::load_or_create_gateway_keypair(db, protocol).await;
242 Self {
243 endpoint_url,
244 keypair,
245 }
246 }
247}
248
249#[bon::bon]
250impl Gateway {
251 #[builder(start_fn = builder, finish_fn = build)]
266 pub async fn new_with_builder(
267 #[builder(start_fn)] lightning_mode: LightningMode,
268 #[builder(start_fn)] client_builder: GatewayClientBuilder,
269 #[builder(start_fn)] gateway_db: Database,
270 bcrypt_password_hash: bcrypt::HashParts,
271 bcrypt_liquidity_manager_password_hash: Option<bcrypt::HashParts>,
272 gateway_state: GatewayState,
273 chain_source: ChainSource,
274 #[builder(default = ([127, 0, 0, 1], 80).into())] listen: SocketAddr,
275 api_addr: Option<SafeUrl>,
276 #[builder(default = DEFAULT_NETWORK)] network: Network,
277 #[builder(default = DEFAULT_NUM_ROUTE_HINTS)] num_route_hints: u32,
278 #[builder(default = PaymentFee::TRANSACTION_FEE_DEFAULT)] default_routing_fees: PaymentFee,
279 #[builder(default = PaymentFee::TRANSACTION_FEE_DEFAULT)]
280 default_transaction_fees: PaymentFee,
281 iroh_listen: Option<SocketAddr>,
282 iroh_dns: Option<SafeUrl>,
283 #[builder(default)] iroh_relays: Vec<SafeUrl>,
284 metrics_listen: Option<SocketAddr>,
285 ) -> anyhow::Result<Gateway> {
286 let versioned_api = api_addr.map(|addr| {
287 addr.join(V1_API_ENDPOINT)
288 .expect("Failed to version gateway API address")
289 });
290
291 let metrics_listen = metrics_listen.unwrap_or_else(|| {
292 SocketAddr::new(
293 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
294 listen.port() + 1,
295 )
296 });
297
298 Gateway::new(
299 lightning_mode,
300 GatewayParameters {
301 listen,
302 versioned_api,
303 bcrypt_password_hash,
304 bcrypt_liquidity_manager_password_hash,
305 network,
306 num_route_hints,
307 default_routing_fees,
308 default_transaction_fees,
309 iroh_listen,
310 iroh_dns,
311 iroh_relays,
312 skip_setup: true,
313 metrics_listen,
314 invoice_rate_limit_burst: DEFAULT_INVOICE_RATE_LIMIT_BURST,
315 invoice_rate_limit_per_second: DEFAULT_INVOICE_RATE_LIMIT_PER_SECOND,
316 },
317 gateway_db,
318 client_builder,
319 gateway_state,
320 chain_source,
321 )
322 .await
323 }
324}
325
326enum ReceivePaymentStreamAction {
328 RetryAfterDelay,
329 NoRetry,
330}
331
332#[derive(Clone)]
333pub struct Gateway {
334 federation_manager: Arc<RwLock<FederationManager>>,
336
337 lightning_mode: LightningMode,
339
340 state: Arc<RwLock<GatewayState>>,
342
343 client_builder: GatewayClientBuilder,
346
347 gateway_db: Database,
349
350 listen: SocketAddr,
352
353 metrics_listen: SocketAddr,
355
356 task_group: TaskGroup,
358
359 bcrypt_password_hash: String,
361
362 bcrypt_liquidity_manager_password_hash: Option<String>,
365
366 num_route_hints: u32,
368
369 network: Network,
371
372 chain_source: ChainSource,
374
375 default_routing_fees: PaymentFee,
377
378 default_transaction_fees: PaymentFee,
380
381 iroh_sk: iroh::SecretKey,
383
384 iroh_listen: Option<SocketAddr>,
386
387 iroh_dns: Option<SafeUrl>,
389
390 iroh_relays: Vec<SafeUrl>,
393
394 registrations: BTreeMap<RegisteredProtocol, Registration>,
397
398 registration_health: RegistrationHealthTracker,
400
401 invoice_rate_limiter: Arc<TokenBucketRateLimiter>,
403}
404
405impl std::fmt::Debug for Gateway {
406 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407 f.debug_struct("Gateway")
408 .field("federation_manager", &self.federation_manager)
409 .field("state", &self.state)
410 .field("client_builder", &self.client_builder)
411 .field("gateway_db", &self.gateway_db)
412 .field("listen", &self.listen)
413 .field("registrations", &self.registrations)
414 .finish_non_exhaustive()
415 }
416}
417
418struct WithdrawDetails {
420 amount: Amount,
421 mint_fees: Option<Amount>,
422 peg_out_fees: PegOutFees,
423}
424
425async fn withdraw_v2(
427 client: &ClientHandleArc,
428 wallet_module: &fedimint_walletv2_client::WalletClientModule,
429 address: &Address,
430 amount: BitcoinAmountOrAll,
431) -> AdminResult<WithdrawResponse> {
432 let fee = wallet_module
433 .send_fee()
434 .await
435 .map_err(|e| AdminGatewayError::WithdrawError {
436 failure_reason: e.fmt_compact().to_string(),
437 })?;
438
439 let withdraw_amount = match amount {
440 BitcoinAmountOrAll::All => {
441 let balance = client.get_balance_for_btc().await.map_err(|err| {
442 AdminGatewayError::Unexpected(anyhow!(
443 "Balance not available: {}",
444 err.fmt_compact()
445 ))
446 })?;
447
448 wallet_module
453 .max_sendable_amount(balance, fee)
454 .await
455 .map_err(|err| AdminGatewayError::WithdrawError {
456 failure_reason: format!(
457 "Insufficient funds. Balance: {balance} Fee: {fee}: {}",
458 err.fmt_compact()
459 ),
460 })?
461 }
462 BitcoinAmountOrAll::Amount(a) => a,
463 };
464
465 let operation_id = wallet_module
466 .send(
467 address.as_unchecked().clone(),
468 withdraw_amount,
469 Some(fee),
470 serde_json::Value::Null,
471 )
472 .await
473 .map_err(|e| AdminGatewayError::WithdrawError {
474 failure_reason: e.fmt_compact().to_string(),
475 })?;
476
477 let result = wallet_module
478 .await_final_send_operation_state(operation_id)
479 .await
480 .map_err(|e| AdminGatewayError::WithdrawError {
481 failure_reason: e.fmt_compact().to_string(),
482 })?;
483
484 let fees = PegOutFees::from_amount(fee);
485
486 match result {
487 fedimint_walletv2_client::FinalSendOperationState::Success(txid) => {
488 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds via walletv2");
489 Ok(WithdrawResponse { txid, fees })
490 }
491 fedimint_walletv2_client::FinalSendOperationState::Aborted => {
492 Err(AdminGatewayError::WithdrawError {
493 failure_reason: "Withdrawal transaction was aborted".to_string(),
494 })
495 }
496 fedimint_walletv2_client::FinalSendOperationState::Failure => {
497 Err(AdminGatewayError::WithdrawError {
498 failure_reason: "Withdrawal failed".to_string(),
499 })
500 }
501 }
502}
503
504async fn calculate_max_withdrawable(
506 client: &ClientHandleArc,
507 address: &Address,
508) -> AdminResult<WithdrawDetails> {
509 let balance = client.get_balance_for_btc().await.map_err(|err| {
510 AdminGatewayError::Unexpected(anyhow!("Balance not available: {}", err.fmt_compact()))
511 })?;
512
513 if let Ok(wallet_module) =
514 client.get_first_module::<fedimint_walletv2_client::WalletClientModule>()
515 {
516 let fee = wallet_module
517 .send_fee()
518 .await
519 .map_err(|e| AdminGatewayError::WithdrawError {
520 failure_reason: e.fmt_compact().to_string(),
521 })?;
522
523 let max_withdrawable = wallet_module
524 .max_sendable_amount(balance, fee)
525 .await
526 .map_err(|err| AdminGatewayError::WithdrawError {
527 failure_reason: err.fmt_compact().to_string(),
528 })?;
529
530 let federation_fees = balance
535 .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
536 .saturating_sub(Amount::from_sats(fee.to_sat()));
537
538 return Ok(WithdrawDetails {
539 amount: Amount::from_sats(max_withdrawable.to_sat()),
540 mint_fees: Some(federation_fees),
541 peg_out_fees: PegOutFees::from_amount(fee),
542 });
543 }
544
545 let Ok(wallet_module) = client.get_first_module::<WalletClientModule>() else {
546 return Err(AdminGatewayError::Unexpected(anyhow!(
547 "No wallet module found"
548 )));
549 };
550
551 let (max_withdrawable, peg_out_fees) = wallet_module
552 .max_withdrawable_amount(address, balance)
553 .await
554 .map_err(|err| AdminGatewayError::WithdrawError {
555 failure_reason: err.fmt_compact().to_string(),
556 })?;
557
558 let federation_fees = balance
563 .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
564 .saturating_sub(Amount::from_sats(peg_out_fees.amount().to_sat()));
565
566 Ok(WithdrawDetails {
567 amount: Amount::from_sats(max_withdrawable.to_sat()),
568 mint_fees: Some(federation_fees),
569 peg_out_fees,
570 })
571}
572
573impl Gateway {
574 fn get_bitcoind_client(
577 opts: &GatewayOpts,
578 network: bitcoin::Network,
579 gateway_id: &PublicKey,
580 ) -> anyhow::Result<(BitcoindClient, ChainSource)> {
581 let bitcoind_username = opts
582 .bitcoind_username
583 .clone()
584 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
585 let url = opts.bitcoind_url.clone().expect("No bitcoind url set");
586 let password = opts
587 .bitcoind_password
588 .clone()
589 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
590
591 let chain_source = ChainSource::Bitcoind {
592 username: bitcoind_username.clone(),
593 password: password.clone(),
594 server_url: url.clone(),
595 };
596 let wallet_name = format!("gatewayd-{gateway_id}");
597 let client = BitcoindClient::new(&url, bitcoind_username, password, &wallet_name, network)?;
598 Ok((client, chain_source))
599 }
600
601 pub async fn new_with_default_modules(
604 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
605 ) -> anyhow::Result<Gateway> {
606 let opts = GatewayOpts::parse();
607 let gateway_parameters = opts.to_gateway_parameters()?;
608 let decoders = ModuleDecoderRegistry::default();
609
610 let db_path = opts.data_dir.join(DB_FILE);
611 let gateway_db = match opts.db_backend {
612 DatabaseBackend::RocksDb => {
613 debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
614 Database::new(
615 fedimint_rocksdb::RocksDb::build(db_path).open().await?,
616 decoders,
617 )
618 }
619 DatabaseBackend::CursedRedb => {
620 debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
621 Database::new(
622 fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
623 decoders,
624 )
625 }
626 };
627
628 apply_migrations(
631 &gateway_db,
632 (),
633 "gatewayd".to_string(),
634 get_gatewayd_database_migrations(),
635 None,
636 None,
637 )
638 .await?;
639
640 let http_id = Self::load_or_create_gateway_keypair(&gateway_db, RegisteredProtocol::Http)
643 .await
644 .public_key();
645 let (dyn_bitcoin_rpc, chain_source) =
646 match (opts.bitcoind_url.as_ref(), opts.esplora_url.as_ref()) {
647 (Some(_), None) => {
648 let (client, chain_source) =
649 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
650 (client.into_dyn(), chain_source)
651 }
652 (None, Some(url)) => {
653 let client = EsploraClient::new(url)
654 .expect("Could not create EsploraClient")
655 .into_dyn();
656 let chain_source = ChainSource::Esplora {
657 server_url: url.clone(),
658 };
659 (client, chain_source)
660 }
661 (Some(_), Some(_)) => {
662 let (client, chain_source) =
664 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
665 (client.into_dyn(), chain_source)
666 }
667 _ => unreachable!("ArgGroup already enforced XOR relation"),
668 };
669
670 let mut registry = ClientModuleInitRegistry::new();
673 registry.attach(MintClientInit);
674 registry.attach(MintV2ClientInit);
675 registry.attach(WalletClientInit::new(dyn_bitcoin_rpc));
676 registry.attach(fedimint_walletv2_client::WalletClientInit);
677
678 let client_builder =
679 GatewayClientBuilder::new(opts.data_dir.clone(), registry, opts.db_backend).await?;
680
681 let gateway_state = if Self::load_mnemonic(&gateway_db).await.is_some() {
682 GatewayState::Disconnected
683 } else {
684 if gateway_parameters.skip_setup {
687 let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
688 info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
689 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
690 |e| {
691 AdminGatewayError::MnemonicError(anyhow!(format!(
692 "Seed phrase provided in environment was invalid {e:?}"
693 )))
694 },
695 )?
696 } else {
697 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
698 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
699 };
700
701 Client::store_encodable_client_secret(&gateway_db, mnemonic.to_entropy())
702 .await
703 .map_err(|err| AdminGatewayError::MnemonicError(err.into()))?;
704 GatewayState::Disconnected
705 } else {
706 GatewayState::NotConfigured { mnemonic_sender }
707 }
708 };
709
710 info!(
711 target: LOG_GATEWAY,
712 version = %fedimint_build_code_version_env!(),
713 "Starting gatewayd",
714 );
715
716 Gateway::new(
717 opts.mode,
718 gateway_parameters,
719 gateway_db,
720 client_builder,
721 gateway_state,
722 chain_source,
723 )
724 .await
725 }
726
727 async fn new(
730 lightning_mode: LightningMode,
731 gateway_parameters: GatewayParameters,
732 gateway_db: Database,
733 client_builder: GatewayClientBuilder,
734 gateway_state: GatewayState,
735 chain_source: ChainSource,
736 ) -> anyhow::Result<Gateway> {
737 let num_route_hints = gateway_parameters.num_route_hints;
738 let network = gateway_parameters.network;
739
740 let task_group = TaskGroup::new();
741 task_group.install_kill_handler();
742
743 let mut registrations = BTreeMap::new();
744 if let Some(http_url) = gateway_parameters.versioned_api {
745 registrations.insert(
746 RegisteredProtocol::Http,
747 Registration::new(&gateway_db, http_url, RegisteredProtocol::Http).await,
748 );
749 }
750
751 let iroh_sk = Self::load_or_create_iroh_key(&gateway_db).await;
752 if gateway_parameters.iroh_listen.is_some() {
753 let endpoint_url = SafeUrl::parse(&format!("iroh://{}", iroh_sk.public()))?;
754 registrations.insert(
755 RegisteredProtocol::Iroh,
756 Registration::new(&gateway_db, endpoint_url, RegisteredProtocol::Iroh).await,
757 );
758 }
759
760 Ok(Self {
761 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
762 lightning_mode,
763 state: Arc::new(RwLock::new(gateway_state)),
764 client_builder,
765 gateway_db: gateway_db.clone(),
766 listen: gateway_parameters.listen,
767 metrics_listen: gateway_parameters.metrics_listen,
768 task_group,
769 bcrypt_password_hash: gateway_parameters.bcrypt_password_hash.to_string(),
770 bcrypt_liquidity_manager_password_hash: gateway_parameters
771 .bcrypt_liquidity_manager_password_hash
772 .map(|h| h.to_string()),
773 num_route_hints,
774 network,
775 chain_source,
776 default_routing_fees: gateway_parameters.default_routing_fees,
777 default_transaction_fees: gateway_parameters.default_transaction_fees,
778 iroh_sk,
779 iroh_dns: gateway_parameters.iroh_dns,
780 iroh_relays: gateway_parameters.iroh_relays,
781 iroh_listen: gateway_parameters.iroh_listen,
782 registrations,
783 registration_health: RegistrationHealthTracker::default(),
784 invoice_rate_limiter: Arc::new(TokenBucketRateLimiter::new(
785 gateway_parameters.invoice_rate_limit_burst,
786 gateway_parameters.invoice_rate_limit_per_second,
787 )),
788 })
789 }
790
791 async fn load_or_create_gateway_keypair(
792 gateway_db: &Database,
793 protocol: RegisteredProtocol,
794 ) -> secp256k1::Keypair {
795 let mut dbtx = gateway_db.begin_transaction().await;
796 let keypair = dbtx.load_or_create_gateway_keypair(protocol).await;
797 dbtx.commit_tx().await;
798 keypair
799 }
800
801 async fn load_or_create_iroh_key(gateway_db: &Database) -> iroh::SecretKey {
804 let mut dbtx = gateway_db.begin_transaction().await;
805 let iroh_sk = dbtx.load_or_create_iroh_key().await;
806 dbtx.commit_tx().await;
807 iroh_sk
808 }
809
810 pub async fn http_gateway_id(&self) -> PublicKey {
811 Self::load_or_create_gateway_keypair(&self.gateway_db, RegisteredProtocol::Http)
812 .await
813 .public_key()
814 }
815
816 async fn get_state(&self) -> GatewayState {
817 self.state.read().await.clone()
818 }
819
820 pub async fn dump_database(
823 dbtx: &mut DatabaseTransaction<'_>,
824 prefix_names: Vec<String>,
825 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
826 dbtx.dump_database(prefix_names).await
827 }
828
829 pub async fn run(
834 self,
835 runtime: Arc<tokio::runtime::Runtime>,
836 mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
837 ) -> anyhow::Result<TaskShutdownToken> {
838 install_crypto_provider().await;
839 self.register_clients_timer();
840 self.load_clients().await?;
841 self.start_gateway(runtime, mnemonic_receiver.resubscribe());
842 self.spawn_backup_task();
843 self.spawn_prune_registered_contracts_task();
844 fedimint_metrics::spawn_api_server(self.metrics_listen, self.task_group.clone()).await?;
846 let handle = self.task_group.make_handle();
848 run_webserver(Arc::new(self), mnemonic_receiver.resubscribe()).await?;
849 let shutdown_receiver = handle.make_shutdown_rx();
850 Ok(shutdown_receiver)
851 }
852
853 fn spawn_backup_task(&self) {
856 let self_copy = self.clone();
857 self.task_group
858 .spawn_cancellable_silent("backup ecash", async move {
859 const BACKUP_UPDATE_INTERVAL: Duration = Duration::from_hours(1);
860 let mut interval = tokio::time::interval(BACKUP_UPDATE_INTERVAL);
861 interval.tick().await;
862 loop {
863 {
864 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
865 self_copy.backup_all_federations(&mut dbtx).await;
866 dbtx.commit_tx().await;
867 interval.tick().await;
868 }
869 }
870 });
871 }
872
873 fn spawn_prune_registered_contracts_task(&self) {
877 let self_copy = self.clone();
878 self.task_group.spawn_cancellable_silent(
879 "prune registered incoming contracts",
880 async move {
881 const PRUNE_INTERVAL: Duration = Duration::from_hours(1);
882 const RETENTION_AFTER_EXPIRY: Duration = Duration::from_hours(24);
886
887 let mut interval = tokio::time::interval(PRUNE_INTERVAL);
888 loop {
889 interval.tick().await;
890
891 let cutoff_secs = duration_since_epoch()
892 .saturating_sub(RETENTION_AFTER_EXPIRY)
893 .as_secs();
894
895 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
896 let num_pruned = dbtx.prune_registered_incoming_contracts(cutoff_secs).await;
897 match dbtx.commit_tx_result().await {
898 Ok(()) => {
899 if num_pruned > 0 {
900 info!(
901 target: LOG_GATEWAY,
902 num_pruned,
903 "Pruned expired incoming contract records"
904 );
905 }
906 }
907 Err(err) => {
908 warn!(
909 target: LOG_GATEWAY,
910 err = %err.fmt_compact(),
911 "Failed to prune expired incoming contract records"
912 );
913 }
914 }
915 }
916 },
917 );
918 }
919
920 pub async fn backup_all_federations(&self, dbtx: &mut DatabaseTransaction<'_, Committable>) {
924 const BACKUP_THRESHOLD_DURATION: Duration = Duration::from_hours(24);
927
928 let now = fedimint_core::time::now();
929 let threshold = now
930 .checked_sub(BACKUP_THRESHOLD_DURATION)
931 .expect("Cannot be negative");
932 for (id, last_backup) in dbtx.load_backup_records().await {
933 match last_backup {
934 Some(backup_time) if backup_time < threshold => {
935 let fed_manager = self.federation_manager.read().await;
936 fed_manager.backup_federation(&id, dbtx, now).await;
937 }
938 None => {
939 let fed_manager = self.federation_manager.read().await;
940 fed_manager.backup_federation(&id, dbtx, now).await;
941 }
942 _ => {}
943 }
944 }
945 }
946
947 fn start_gateway(
950 &self,
951 runtime: Arc<tokio::runtime::Runtime>,
952 mut mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
953 ) {
954 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 60;
955
956 let self_copy = self.clone();
957 let tg = self.task_group.clone();
958 self.task_group.spawn(
959 "Subscribe to intercepted lightning payments in stream",
960 |handle| async move {
961 loop {
963 if handle.is_shutting_down() {
964 info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
965 break;
966 }
967
968 if let GatewayState::NotConfigured{ .. } = self_copy.get_state().await {
969 info!(
970 target: LOG_GATEWAY,
971 "Waiting for the mnemonic to be set before starting lightning receive loop."
972 );
973 info!(
974 target: LOG_GATEWAY,
975 "You might need to provide it from the UI or refer to documentation w.r.t how to initialize it."
976 );
977
978 let _ = mnemonic_receiver.recv().await;
979 info!(
980 target: LOG_GATEWAY,
981 "Received mnemonic, attempting to start lightning receive loop"
982 );
983 }
984
985 let payment_stream_task_group = tg.make_subgroup();
986 let lnrpc_route = self_copy.create_lightning_client(runtime.clone()).await;
987
988 debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
989 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
990 {
991 Ok((stream, ln_client)) => (stream, ln_client),
992 Err(err) => {
993 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
994 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
1002 crit!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Lightning payment stream task group shutdown");
1003 }
1004 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
1005 continue
1006 }
1007 };
1008
1009 self_copy.set_gateway_state(GatewayState::Connected).await;
1011 info!(target: LOG_GATEWAY, "Established lightning payment stream");
1012
1013 let route_payments_response =
1014 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
1015
1016 self_copy.set_gateway_state(GatewayState::Disconnected).await;
1017 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
1018 crit!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Lightning payment stream task group shutdown");
1019 }
1020
1021 self_copy.unannounce_from_all_federations().await;
1022
1023 match route_payments_response {
1024 ReceivePaymentStreamAction::RetryAfterDelay => {
1025 warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
1026 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
1027 }
1028 ReceivePaymentStreamAction::NoRetry => break,
1029 }
1030 }
1031 },
1032 );
1033 }
1034
1035 async fn route_lightning_payments<'a>(
1039 &'a self,
1040 handle: &TaskHandle,
1041 mut stream: RouteHtlcStream<'a>,
1042 ln_client: Arc<dyn ILnRpcClient>,
1043 ) -> ReceivePaymentStreamAction {
1044 let LightningInfo::Connected {
1045 public_key: lightning_public_key,
1046 alias: lightning_alias,
1047 network: lightning_network,
1048 block_height: _,
1049 synced_to_chain,
1050 } = ln_client.parsed_node_info().await
1051 else {
1052 warn!(target: LOG_GATEWAY, "Failed to retrieve Lightning info");
1053 return ReceivePaymentStreamAction::RetryAfterDelay;
1054 };
1055
1056 assert!(
1057 self.network == lightning_network,
1058 "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
1059 self.network
1060 );
1061
1062 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
1063 info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
1064 } else {
1065 self.set_gateway_state(GatewayState::Syncing).await;
1066 info!(target: LOG_GATEWAY, "Waiting for chain sync");
1067 if let Err(err) = ln_client.wait_for_chain_sync().await {
1068 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
1069 return ReceivePaymentStreamAction::RetryAfterDelay;
1070 }
1071 }
1072
1073 let lightning_context = LightningContext {
1074 lnrpc: LnRpcTracked::new(ln_client, "gateway"),
1075 lightning_public_key,
1076 lightning_alias,
1077 lightning_network,
1078 };
1079 if let GatewayState::ShuttingDown { .. } = self
1080 .set_gateway_state(GatewayState::Running { lightning_context })
1081 .await
1082 {
1083 info!(
1084 target: LOG_GATEWAY,
1085 "Reconnected to the lightning node while shutting down, not accepting payments"
1086 );
1087 } else {
1088 info!(target: LOG_GATEWAY, "Gateway is running");
1089 }
1090
1091 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1092 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1095 let all_federations_configs =
1096 dbtx.load_federation_configs().await.into_iter().collect();
1097 self.register_federations(&all_federations_configs, &self.task_group)
1098 .await;
1099 }
1100
1101 let htlc_task_group = self.task_group.make_subgroup();
1104 if handle
1105 .cancel_on_shutdown(async move {
1106 loop {
1107 let payment_request_or = tokio::select! {
1108 payment_request_or = stream.next() => {
1109 payment_request_or
1110 }
1111 () = self.is_shutting_down_safely() => {
1112 break;
1113 }
1114 };
1115
1116 let Some(payment_request) = payment_request_or else {
1117 warn!(
1118 target: LOG_GATEWAY,
1119 "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
1120 );
1121 break;
1122 };
1123
1124 let state_guard = self.state.read().await;
1125 if let GatewayState::Running { ref lightning_context } = *state_guard {
1126 let gateway = self.clone();
1128 let lightning_context = lightning_context.clone();
1129 htlc_task_group.spawn_cancellable_silent(
1130 "handle_lightning_payment",
1131 async move {
1132 let start = fedimint_core::time::now();
1133 let outcome = gateway
1134 .handle_lightning_payment(payment_request, &lightning_context)
1135 .await;
1136 metrics::HTLC_HANDLING_DURATION_SECONDS
1137 .with_label_values(&[outcome])
1138 .observe(
1139 fedimint_core::time::now()
1140 .duration_since(start)
1141 .unwrap_or_default()
1142 .as_secs_f64(),
1143 );
1144 },
1145 );
1146 } else {
1147 warn!(
1148 target: LOG_GATEWAY,
1149 state = %state_guard,
1150 "Gateway isn't in a running state, cannot handle incoming payments."
1151 );
1152 break;
1153 }
1154 }
1155 })
1156 .await
1157 .is_ok()
1158 {
1159 warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
1160 ReceivePaymentStreamAction::RetryAfterDelay
1161 } else {
1162 info!(target: LOG_GATEWAY, "Received shutdown signal");
1163 ReceivePaymentStreamAction::NoRetry
1164 }
1165 }
1166
1167 async fn is_shutting_down_safely(&self) {
1170 loop {
1171 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
1172 return;
1173 }
1174
1175 fedimint_core::task::sleep(Duration::from_secs(1)).await;
1176 }
1177 }
1178
1179 pub async fn handle_lightning_payment(
1189 &self,
1190 payment_request: InterceptPaymentRequest,
1191 lightning_context: &LightningContext,
1192 ) -> &'static str {
1193 info!(
1194 target: LOG_GATEWAY,
1195 lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
1196 "Intercepting lightning payment",
1197 );
1198
1199 let lnv2_start = fedimint_core::time::now();
1200 let lnv2_result = self
1201 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
1202 .await;
1203 let lnv2_outcome = if lnv2_result.is_ok() {
1204 "success"
1205 } else {
1206 "error"
1207 };
1208 metrics::HTLC_LNV2_ATTEMPT_DURATION_SECONDS
1209 .with_label_values(&[lnv2_outcome])
1210 .observe(
1211 fedimint_core::time::now()
1212 .duration_since(lnv2_start)
1213 .unwrap_or_default()
1214 .as_secs_f64(),
1215 );
1216 if lnv2_result.is_ok() {
1217 return "lnv2";
1218 }
1219
1220 let lnv1_start = fedimint_core::time::now();
1221 let lnv1_result = self
1222 .try_handle_lightning_payment_ln_legacy(&payment_request, lightning_context)
1223 .await;
1224 let lnv1_outcome = if lnv1_result.is_ok() {
1225 "success"
1226 } else {
1227 "error"
1228 };
1229 metrics::HTLC_LNV1_ATTEMPT_DURATION_SECONDS
1230 .with_label_values(&[lnv1_outcome])
1231 .observe(
1232 fedimint_core::time::now()
1233 .duration_since(lnv1_start)
1234 .unwrap_or_default()
1235 .as_secs_f64(),
1236 );
1237 if lnv1_result.is_ok() {
1238 return "lnv1";
1239 }
1240
1241 let is_federation_scid = match payment_request.short_channel_id {
1246 Some(scid) => self
1247 .federation_manager
1248 .read()
1249 .await
1250 .get_client_for_index(scid)
1251 .is_some(),
1252 None => false,
1253 };
1254
1255 let receive_disabled = [&lnv2_result, &lnv1_result]
1261 .into_iter()
1262 .any(|result| matches!(result, Err(PublicGatewayError::ReceiveDisabled { .. })));
1263
1264 if is_federation_scid || receive_disabled {
1265 warn!(
1272 target: LOG_GATEWAY,
1273 payment_hash = %payment_request.payment_hash,
1274 short_channel_id = ?payment_request.short_channel_id,
1275 amount_msat = payment_request.amount_msat,
1276 incoming_chan_id = payment_request.incoming_chan_id,
1277 htlc_id = payment_request.htlc_id,
1278 receive_disabled,
1279 lnv2_err = ?lnv2_result.as_ref().err(),
1280 lnv1_err = ?lnv1_result.as_ref().err(),
1281 "Lightning payment for a served federation could not be accepted: cancelling HTLC",
1282 );
1283 Self::cancel_unmatched_lightning_payment(payment_request, lightning_context).await;
1284 "cancel"
1285 } else {
1286 Self::forward_lightning_payment(payment_request, lightning_context).await;
1291 "forward"
1292 }
1293 }
1294
1295 async fn try_handle_lightning_payment_lnv2(
1298 &self,
1299 htlc_request: &InterceptPaymentRequest,
1300 lightning_context: &LightningContext,
1301 ) -> Result<()> {
1302 let (contract, client) = self
1312 .get_registered_incoming_contract_and_client_v2(
1313 PaymentImage::Hash(htlc_request.payment_hash),
1314 htlc_request.incoming_amount_msat,
1315 )
1316 .await?;
1317
1318 if let Err(err) = client
1319 .get_first_module::<GatewayClientModuleV2>()
1320 .expect("Must have client module")
1321 .relay_incoming_htlc(
1322 htlc_request.payment_hash,
1323 htlc_request.incoming_chan_id,
1324 htlc_request.htlc_id,
1325 contract,
1326 htlc_request.incoming_amount_msat,
1327 )
1328 .await
1329 {
1330 warn!(
1331 target: LOG_GATEWAY,
1332 err = %err.fmt_compact(),
1333 "Error relaying incoming lightning payment"
1334 );
1335
1336 let outcome = InterceptPaymentResponse {
1337 action: PaymentAction::Cancel,
1338 payment_hash: htlc_request.payment_hash,
1339 incoming_chan_id: htlc_request.incoming_chan_id,
1340 htlc_id: htlc_request.htlc_id,
1341 };
1342
1343 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1344 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
1345 }
1346 }
1347
1348 Ok(())
1349 }
1350
1351 async fn try_handle_lightning_payment_ln_legacy(
1354 &self,
1355 htlc_request: &InterceptPaymentRequest,
1356 lightning_context: &LightningContext,
1357 ) -> Result<()> {
1358 let Some(federation_index) = htlc_request.short_channel_id else {
1360 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1361 "Incoming payment has not last hop short channel id".to_string(),
1362 )));
1363 };
1364
1365 let Some(client) = self
1366 .federation_manager
1367 .read()
1368 .await
1369 .get_client_for_index(federation_index)
1370 else {
1371 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())));
1372 };
1373
1374 let federation_id = client.borrow().with_sync(|client| client.federation_id());
1378 if !self.receive_enabled(federation_id).await {
1379 return Err(PublicGatewayError::ReceiveDisabled {
1380 federation_id_prefix: federation_id.to_prefix(),
1381 });
1382 }
1383
1384 client
1389 .borrow()
1390 .with(|client| async {
1391 let htlc = Htlc::from(htlc_request.clone());
1392 let lnv1 = client
1393 .get_first_module::<GatewayClientModule>()
1394 .map_err(|_| {
1395 PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1396 "Federation does not have LNv1 module".to_string(),
1397 ))
1398 })?;
1399 match lnv1
1400 .gateway_handle_intercepted_htlc(htlc, async {
1401 Ok(lightning_context.lnrpc.info().await?.block_height)
1402 })
1403 .await
1404 {
1405 Ok(_) => Ok(()),
1406 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1407 format!("Error intercepting lightning payment {}", e.fmt_compact()),
1408 ))),
1409 }
1410 })
1411 .await
1412 }
1413
1414 async fn cancel_unmatched_lightning_payment(
1428 htlc_request: InterceptPaymentRequest,
1429 lightning_context: &LightningContext,
1430 ) {
1431 let outcome = InterceptPaymentResponse {
1432 action: PaymentAction::Cancel,
1433 payment_hash: htlc_request.payment_hash,
1434 incoming_chan_id: htlc_request.incoming_chan_id,
1435 htlc_id: htlc_request.htlc_id,
1436 };
1437
1438 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1439 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1440 }
1441 }
1442
1443 async fn forward_lightning_payment(
1448 htlc_request: InterceptPaymentRequest,
1449 lightning_context: &LightningContext,
1450 ) {
1451 let outcome = InterceptPaymentResponse {
1452 action: PaymentAction::Forward,
1453 payment_hash: htlc_request.payment_hash,
1454 incoming_chan_id: htlc_request.incoming_chan_id,
1455 htlc_id: htlc_request.htlc_id,
1456 };
1457
1458 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1459 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1460 }
1461 }
1462
1463 async fn set_gateway_state(&self, state: GatewayState) -> GatewayState {
1474 let mut lock = self.state.write().await;
1475
1476 if let GatewayState::ShuttingDown { .. } = *lock {
1477 match state {
1478 GatewayState::Running { lightning_context } => {
1479 *lock = GatewayState::ShuttingDown { lightning_context };
1480 }
1481 ignored => {
1482 info!(
1483 target: LOG_GATEWAY,
1484 ignored_state = %ignored,
1485 "Gateway is shutting down, ignoring state change"
1486 );
1487 }
1488 }
1489 } else {
1490 *lock = state;
1491 }
1492
1493 lock.clone()
1494 }
1495
1496 #[doc(hidden)]
1504 pub async fn set_gateway_state_out_of_band(&self, state: GatewayState) {
1505 self.set_gateway_state(state).await;
1506 }
1507
1508 pub async fn handle_get_federation_config(
1511 &self,
1512 federation_id_or: Option<FederationId>,
1513 ) -> AdminResult<GatewayFedConfig> {
1514 if !matches!(self.get_state().await, GatewayState::Running { .. }) {
1515 return Ok(GatewayFedConfig {
1516 federations: BTreeMap::new(),
1517 });
1518 }
1519
1520 let federations = if let Some(federation_id) = federation_id_or {
1521 let mut federations = BTreeMap::new();
1522 federations.insert(
1523 federation_id,
1524 self.federation_manager
1525 .read()
1526 .await
1527 .get_federation_config(federation_id)
1528 .await?,
1529 );
1530 federations
1531 } else {
1532 self.federation_manager
1533 .read()
1534 .await
1535 .get_all_federation_configs()
1536 .await
1537 };
1538
1539 Ok(GatewayFedConfig { federations })
1540 }
1541
1542 pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
1545 let client = self.select_client(payload.federation_id).await?;
1546
1547 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1548 let address = wallet_module
1549 .allocate_deposit_address_expert_only(())
1550 .await
1551 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?
1552 .address;
1553 Ok(address)
1554 } else if let Ok(wallet_module) = client
1555 .value()
1556 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1557 {
1558 Ok(wallet_module.receive().await)
1559 } else {
1560 Err(AdminGatewayError::Unexpected(anyhow!(
1561 "No wallet module found"
1562 )))
1563 }
1564 }
1565
1566 async fn handle_pay_invoice_msg(
1569 &self,
1570 payload: fedimint_ln_client::pay::PayInvoicePayload,
1571 ) -> Result<Preimage> {
1572 let GatewayState::Running { .. } = self.get_state().await else {
1573 return Err(PublicGatewayError::Lightning(
1574 LightningRpcError::FailedToConnect,
1575 ));
1576 };
1577
1578 debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1579 let client = self.select_client(payload.federation_id).await?;
1580 let contract_id = payload.contract_id;
1581 let gateway_module = &client
1582 .value()
1583 .get_first_module::<GatewayClientModule>()
1584 .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1585 .map_err(PublicGatewayError::LNv1)?;
1586 let operation_id = gateway_module
1587 .gateway_pay_bolt11_invoice(payload)
1588 .await
1589 .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1590 .map_err(PublicGatewayError::LNv1)?;
1591 let mut updates = gateway_module
1592 .gateway_subscribe_ln_pay(operation_id)
1593 .await
1594 .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1595 .map_err(PublicGatewayError::LNv1)?
1596 .into_stream();
1597 while let Some(update) = updates.next().await {
1598 match update {
1599 GatewayExtPayStates::Success { preimage, .. } => {
1600 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1601 return Ok(preimage);
1602 }
1603 GatewayExtPayStates::Fail {
1604 error,
1605 error_message,
1606 } => {
1607 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1608 error: Box::new(error),
1609 message: format!(
1610 "{error_message} while paying invoice with contract id {contract_id}"
1611 ),
1612 }));
1613 }
1614 GatewayExtPayStates::Canceled { error } => {
1615 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1616 error: Box::new(error.clone()),
1617 message: format!(
1618 "Cancelled with {error} while paying invoice with contract id {contract_id}"
1619 ),
1620 }));
1621 }
1622 GatewayExtPayStates::Created => {
1623 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1624 }
1625 other => {
1626 debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1627 }
1628 }
1629 }
1630
1631 Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1632 anyhow!("Ran out of state updates while paying invoice"),
1633 )))
1634 }
1635
1636 pub async fn handle_backup_msg(
1639 &self,
1640 BackupPayload { federation_id }: BackupPayload,
1641 ) -> AdminResult<()> {
1642 let federation_manager = self.federation_manager.read().await;
1643 let client = federation_manager
1644 .client(&federation_id)
1645 .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1646 format!("Gateway has not connected to {federation_id}")
1647 )))?
1648 .value();
1649 let metadata: BTreeMap<String, String> = BTreeMap::new();
1650 #[allow(deprecated)]
1651 client
1652 .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1653 metadata,
1654 ))
1655 .await
1656 .map_err(anyhow::Error::from)?;
1657 Ok(())
1658 }
1659
1660 pub async fn handle_recheck_address_msg(
1662 &self,
1663 payload: DepositAddressRecheckPayload,
1664 ) -> AdminResult<()> {
1665 let client = self.select_client(payload.federation_id).await?;
1666
1667 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1668 wallet_module
1669 .recheck_pegin_address_by_address(payload.address)
1670 .await
1671 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
1672 Ok(())
1673 } else if client
1674 .value()
1675 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1676 .is_ok()
1677 {
1678 Ok(())
1680 } else {
1681 Err(AdminGatewayError::Unexpected(anyhow!(
1682 "No wallet module found"
1683 )))
1684 }
1685 }
1686
1687 pub async fn handle_receive_ecash_msg(
1689 &self,
1690 payload: ReceiveEcashPayload,
1691 ) -> Result<ReceiveEcashResponse> {
1692 let federation_id_prefix = base32::decode_prefixed::<fedimint_mintv2_client::ECash>(
1694 FEDIMINT_PREFIX,
1695 &payload.notes,
1696 )
1697 .ok()
1698 .and_then(|e| e.mint())
1699 .map(|id| id.to_prefix())
1700 .or_else(|| {
1701 OOBNotes::from_str(&payload.notes)
1702 .ok()
1703 .map(|n| n.federation_id_prefix())
1704 })
1705 .ok_or_else(|| PublicGatewayError::ReceiveEcashError {
1706 failure_reason: "Invalid ecash format: could not parse as ECash or OOBNotes"
1707 .to_string(),
1708 })?;
1709
1710 let client = self
1711 .federation_manager
1712 .read()
1713 .await
1714 .get_client_for_federation_id_prefix(federation_id_prefix)
1715 .ok_or(FederationNotConnected {
1716 federation_id_prefix,
1717 })?;
1718
1719 if let Ok(mint) = client.value().get_first_module::<MintClientModule>() {
1721 let notes = OOBNotes::from_str(&payload.notes).map_err(|e| {
1722 PublicGatewayError::ReceiveEcashError {
1723 failure_reason: format!(
1724 "Expected OOBNotes for MintV1 federation: {}",
1725 e.fmt_compact()
1726 ),
1727 }
1728 })?;
1729 let amount = notes.total_amount();
1730
1731 let operation_id = mint.reissue_external_notes(notes, ()).await.map_err(|e| {
1732 PublicGatewayError::ReceiveEcashError {
1733 failure_reason: e.fmt_compact().to_string(),
1734 }
1735 })?;
1736
1737 let mut updates = mint
1738 .subscribe_reissue_external_notes(operation_id)
1739 .await
1740 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1741 failure_reason: format!(
1742 "Could not subscribe to reissue operation: {}",
1743 e.fmt_compact()
1744 ),
1745 })?
1746 .into_stream();
1747
1748 let mut reissued = false;
1753 while let Some(update) = updates.next().await {
1754 match update {
1755 ReissueExternalNotesState::Failed(failure_reason) => {
1756 return Err(PublicGatewayError::ReceiveEcashError { failure_reason });
1757 }
1758 ReissueExternalNotesState::Done => reissued = true,
1759 ReissueExternalNotesState::Created | ReissueExternalNotesState::Issuing => {}
1760 }
1761 }
1762
1763 if !reissued {
1764 return Err(PublicGatewayError::ReceiveEcashError {
1765 failure_reason: "Reissue operation ended before the notes were reissued"
1766 .to_string(),
1767 });
1768 }
1769
1770 Ok(ReceiveEcashResponse { amount })
1771 } else if let Ok(mint) = client
1772 .value()
1773 .get_primary_module_for_unit::<MintV2ClientModule>(
1774 fedimint_core::module::AmountUnit::BITCOIN,
1775 )
1776 {
1777 let ecash: fedimint_mintv2_client::ECash =
1778 base32::decode_prefixed(FEDIMINT_PREFIX, &payload.notes).map_err(|e| {
1779 PublicGatewayError::ReceiveEcashError {
1780 failure_reason: format!("Expected ECash for MintV2 federation: {e}"),
1781 }
1782 })?;
1783 let amount = ecash.amount();
1784
1785 let operation_id = mint
1786 .receive(ecash, serde_json::Value::Null)
1787 .await
1788 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1789 failure_reason: e.fmt_compact().to_string(),
1790 })?;
1791
1792 let final_state = mint
1793 .await_final_receive_operation_state(operation_id)
1794 .await
1795 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1796 failure_reason: e.fmt_compact().to_string(),
1797 })?;
1798 match final_state {
1799 fedimint_mintv2_client::FinalReceiveOperationState::Success => {}
1800 fedimint_mintv2_client::FinalReceiveOperationState::Rejected => {
1801 return Err(PublicGatewayError::ReceiveEcashError {
1802 failure_reason: "ECash receive was rejected".to_string(),
1803 });
1804 }
1805 }
1806
1807 Ok(ReceiveEcashResponse { amount })
1808 } else {
1809 Err(PublicGatewayError::ReceiveEcashError {
1810 failure_reason: "No mint module found".to_string(),
1811 })
1812 }
1813 }
1814
1815 pub async fn handle_get_invoice_msg(
1818 &self,
1819 payload: GetInvoiceRequest,
1820 ) -> AdminResult<Option<GetInvoiceResponse>> {
1821 let lightning_context = self.get_lightning_context().await?;
1822 let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1823 Ok(invoice)
1824 }
1825
1826 pub async fn handle_withdraw_to_onchain_msg(
1829 &self,
1830 payload: WithdrawToOnchainPayload,
1831 ) -> AdminResult<WithdrawResponse> {
1832 let address = self.handle_get_ln_onchain_address_msg().await?;
1833 let withdraw = WithdrawPayload {
1834 address: address.into_unchecked(),
1835 federation_id: payload.federation_id,
1836 amount: payload.amount,
1837 quoted_fees: None,
1838 };
1839 self.handle_withdraw_msg(withdraw).await
1840 }
1841
1842 pub async fn handle_pegin_from_onchain_msg(
1845 &self,
1846 payload: PeginFromOnchainPayload,
1847 ) -> AdminResult<Txid> {
1848 let deposit = DepositAddressPayload {
1849 federation_id: payload.federation_id,
1850 };
1851 let address = self.handle_address_msg(deposit).await?;
1852 let send_onchain = SendOnchainRequest {
1853 address: address.into_unchecked(),
1854 amount: payload.amount,
1855 fee_rate_sats_per_vbyte: payload.fee_rate_sats_per_vbyte,
1856 };
1857 let txid = self.handle_send_onchain_msg(send_onchain).await?;
1858
1859 Ok(txid)
1860 }
1861
1862 async fn register_federations(
1870 &self,
1871 federations: &BTreeMap<FederationId, FederationConfig>,
1872 register_task_group: &TaskGroup,
1873 ) {
1874 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
1875 info!(
1876 target: LOG_GATEWAY,
1877 "Gateway is shutting down, skipping federation registration"
1878 );
1879 return;
1880 }
1881
1882 if let Ok(lightning_context) = self.get_lightning_context().await {
1883 let route_hints = lightning_context
1884 .lnrpc
1885 .parsed_route_hints(self.num_route_hints)
1886 .await;
1887 if route_hints.is_empty() {
1888 warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
1889 }
1890
1891 for (federation_id, federation_config) in federations {
1892 let routing_fees = match RoutingFees::try_from(federation_config.lightning_fee) {
1897 Ok(routing_fees) => routing_fees,
1898 Err(err) => {
1899 warn!(
1900 target: LOG_GATEWAY,
1901 %federation_id,
1902 err = %err.fmt_compact(),
1903 "Skipping registration, the configured lightning fee cannot be announced. Set a smaller fee with `set_fees`."
1904 );
1905 continue;
1906 }
1907 };
1908
1909 let fed_manager = self.federation_manager.read().await;
1910 if let Some(client) = fed_manager.client(federation_id) {
1911 let federation_id = *federation_id;
1912 let client_arc = client.clone().into_value();
1913 let route_hints = route_hints.clone();
1914 let lightning_context = lightning_context.clone();
1915 let registration_health = self.registration_health.clone();
1916 let registrations = self
1917 .registrations
1918 .clone()
1919 .into_iter()
1920 .map(|(protocol, registration)| {
1921 let attempt =
1922 registration_health.begin_lnv1_attempt(federation_id, protocol);
1923 (registration, attempt)
1924 })
1925 .collect::<Vec<_>>();
1926
1927 register_task_group.spawn_cancellable_silent(
1928 "register federation",
1929 async move {
1930 let Ok(gateway_client) =
1931 client_arc.get_first_module::<GatewayClientModule>()
1932 else {
1933 return;
1934 };
1935
1936 for (registration, attempt) in registrations {
1937 let succeeded = gateway_client
1938 .try_register_with_federation(
1939 route_hints.clone(),
1940 GW_ANNOUNCEMENT_TTL,
1941 routing_fees,
1942 lightning_context.clone(),
1943 registration.endpoint_url,
1944 registration.keypair,
1945 )
1946 .await;
1947 registration_health
1948 .complete_attempt(
1949 attempt,
1950 succeeded,
1951 fedimint_core::time::now(),
1952 fedimint_core::runtime::Instant::now(),
1953 )
1954 .await;
1955 }
1956 },
1957 );
1958 }
1959 }
1960 }
1961 }
1962
1963 pub async fn select_client(
1966 &self,
1967 federation_id: FederationId,
1968 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
1969 {
1970 self.federation_manager
1971 .read()
1972 .await
1973 .client(&federation_id)
1974 .cloned()
1975 .ok_or(FederationNotConnected {
1976 federation_id_prefix: federation_id.to_prefix(),
1977 })
1978 }
1979
1980 async fn load_mnemonic(gateway_db: &Database) -> Option<Mnemonic> {
1981 let secret = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db)
1982 .await
1983 .ok()?;
1984 Mnemonic::from_entropy(&secret).ok()
1985 }
1986
1987 async fn load_clients(&self) -> AdminResult<()> {
1991 if let GatewayState::NotConfigured { .. } = self.get_state().await {
1992 return Ok(());
1993 }
1994
1995 let mut federation_manager = self.federation_manager.write().await;
1996
1997 let configs = {
1998 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1999 dbtx.load_federation_configs().await
2000 };
2001
2002 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
2003 federation_manager.set_next_index(max_federation_index + 1);
2004 }
2005
2006 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2007 .await
2008 .expect("mnemonic should be set");
2009
2010 for (federation_id, config) in configs {
2011 let federation_index = config.federation_index;
2012 match Box::pin(Spanned::try_new(
2013 info_span!(target: LOG_GATEWAY, "client", federation_id = %federation_id.clone()),
2014 self.client_builder
2015 .build(config, Arc::new(self.clone()), &mnemonic),
2016 ))
2017 .await
2018 {
2019 Ok(client) => {
2020 federation_manager.add_client(federation_index, client);
2021 }
2022 _ => {
2023 warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
2024 }
2025 }
2026 }
2027
2028 Ok(())
2029 }
2030
2031 fn register_clients_timer(&self) {
2037 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2039 info!(target: LOG_GATEWAY, "Spawning register task...");
2040 let gateway = self.clone();
2041 let register_task_group = self.task_group.make_subgroup();
2042 self.task_group.spawn_cancellable("register clients", async move {
2043 loop {
2044 let gateway_state = gateway.get_state().await;
2045 if let GatewayState::Running { .. } = &gateway_state {
2046 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
2047 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
2048 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
2049 } else {
2050 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
2052 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");
2053 sleep(NOT_RUNNING_RETRY).await;
2054 continue;
2055 }
2056
2057 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
2060 }
2061 });
2062 }
2063 }
2064
2065 async fn check_federation_network(
2068 client: &ClientHandleArc,
2069 network: Network,
2070 ) -> AdminResult<()> {
2071 let federation_id = client.federation_id();
2072 let config = client.config().await;
2073
2074 let lnv1_cfg = config
2075 .modules
2076 .values()
2077 .find(|m| LightningCommonInit::KIND == m.kind);
2078
2079 let lnv2_cfg = config
2080 .modules
2081 .values()
2082 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
2083
2084 if lnv1_cfg.is_none() && lnv2_cfg.is_none() {
2086 return Err(AdminGatewayError::ClientCreationError(anyhow!(
2087 "Federation {federation_id} does not have any lightning module (LNv1 or LNv2)"
2088 )));
2089 }
2090
2091 if let Some(cfg) = lnv1_cfg {
2093 let ln_cfg: &LightningClientConfig = cfg.cast().map_err(anyhow::Error::from)?;
2094
2095 if ln_cfg.network.0 != network {
2096 crit!(
2097 target: LOG_GATEWAY,
2098 federation_id = %federation_id,
2099 network = %network,
2100 "Incorrect LNv1 network for federation",
2101 );
2102 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2103 "Unsupported LNv1 network {}",
2104 ln_cfg.network
2105 ))));
2106 }
2107 }
2108
2109 if let Some(cfg) = lnv2_cfg {
2111 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig =
2112 cfg.cast().map_err(anyhow::Error::from)?;
2113
2114 if ln_cfg.network != network {
2115 crit!(
2116 target: LOG_GATEWAY,
2117 federation_id = %federation_id,
2118 network = %network,
2119 "Incorrect LNv2 network for federation",
2120 );
2121 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2122 "Unsupported LNv2 network {}",
2123 ln_cfg.network
2124 ))));
2125 }
2126 }
2127
2128 Ok(())
2129 }
2130
2131 pub async fn get_lightning_context(
2141 &self,
2142 ) -> std::result::Result<LightningContext, LightningRpcError> {
2143 match self.get_state().await {
2144 GatewayState::Running { lightning_context }
2145 | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
2146 _ => Err(LightningRpcError::FailedToConnect),
2147 }
2148 }
2149
2150 async fn await_lightning_context(&self) -> LightningContext {
2171 loop {
2172 match self.get_lightning_context().await {
2173 Ok(lightning_context) => return lightning_context,
2174 Err(err) => {
2175 let state = self.get_state().await;
2176
2177 warn!(
2178 target: LOG_GATEWAY,
2179 err = %err.fmt_compact(),
2180 %state,
2181 retry_interval_secs = LIGHTNING_CONTEXT_RETRY_INTERVAL.as_secs(),
2182 "Not connected to the lightning node, waiting before asking it again",
2183 );
2184
2185 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
2186 }
2187 }
2188 }
2189 }
2190
2191 pub async fn unannounce_from_all_federations(&self) {
2194 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2195 for registration in self.registrations.values() {
2196 self.federation_manager
2197 .read()
2198 .await
2199 .unannounce_from_all_federations(registration.keypair)
2200 .await;
2201 }
2202 }
2203 }
2204
2205 async fn create_lightning_client(
2206 &self,
2207 runtime: Arc<tokio::runtime::Runtime>,
2208 ) -> Box<dyn ILnRpcClient> {
2209 match self.lightning_mode.clone() {
2210 LightningMode::Lnd {
2211 lnd_rpc_addr,
2212 lnd_tls_cert,
2213 lnd_macaroon,
2214 lnd_time_pref,
2215 lnd_payment_timeout_secs,
2216 } => {
2217 let gateway_db = self.gateway_db.clone();
2222 let lnv2_filter: Lnv2HoldInvoiceFilter = Arc::new(move |hash| {
2223 let gateway_db = gateway_db.clone();
2224 Box::pin(async move {
2225 gateway_db
2226 .begin_transaction_nc()
2227 .await
2228 .load_registered_incoming_contract(PaymentImage::Hash(hash))
2229 .await
2230 .is_some()
2231 })
2232 });
2233
2234 Box::new(GatewayLndClient::new(
2235 lnd_rpc_addr,
2236 lnd_tls_cert,
2237 lnd_macaroon,
2238 lnd_time_pref,
2239 lnd_payment_timeout_secs,
2240 None,
2241 lnv2_filter,
2242 ))
2243 }
2244 LightningMode::Ldk {
2245 lightning_port,
2246 alias,
2247 } => {
2248 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2249 .await
2250 .expect("mnemonic should be set");
2251 retry("create LDK Node", fibonacci_max_one_hour(), || async {
2255 ldk::GatewayLdkClient::new(
2256 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
2257 self.chain_source.clone(),
2258 self.network,
2259 lightning_port,
2260 alias.clone(),
2261 mnemonic.clone(),
2262 runtime.clone(),
2263 )
2264 .map(Box::new)
2265 .map_err(anyhow::Error::from)
2268 })
2269 .await
2270 .expect("Could not create LDK Node")
2271 }
2272 }
2273 }
2274}
2275
2276#[async_trait]
2277impl IAdminGateway for Gateway {
2278 type Error = AdminGatewayError;
2279
2280 async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
2283 let GatewayState::Running { lightning_context } = self.get_state().await else {
2284 return Ok(GatewayInfo {
2285 federations: vec![],
2286 federation_fake_scids: None,
2287 version_hash: fedimint_build_code_version_env!().to_string(),
2288 gateway_state: self.state.read().await.to_string(),
2289 lightning_info: LightningInfo::NotConnected,
2290 lightning_mode: self.lightning_mode.clone(),
2291 registrations: self
2292 .registrations
2293 .iter()
2294 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2295 .collect(),
2296 });
2297 };
2298
2299 let dbtx = self.gateway_db.begin_transaction_nc().await;
2300 let federations = self
2301 .federation_manager
2302 .read()
2303 .await
2304 .federation_info_all_federations(dbtx)
2305 .await;
2306
2307 let channels: BTreeMap<u64, FederationId> = federations
2308 .iter()
2309 .map(|federation_info| {
2310 (
2311 federation_info.config.federation_index,
2312 federation_info.federation_id,
2313 )
2314 })
2315 .collect();
2316
2317 let lightning_info = lightning_context.lnrpc.parsed_node_info().await;
2318
2319 Ok(GatewayInfo {
2320 federations,
2321 federation_fake_scids: Some(channels),
2322 version_hash: fedimint_build_code_version_env!().to_string(),
2323 gateway_state: self.state.read().await.to_string(),
2324 lightning_info,
2325 lightning_mode: self.lightning_mode.clone(),
2326 registrations: self
2327 .registrations
2328 .iter()
2329 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2330 .collect(),
2331 })
2332 }
2333
2334 async fn handle_list_channels_msg(
2337 &self,
2338 ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
2339 let context = self.get_lightning_context().await?;
2340 let response = context.lnrpc.list_channels().await?;
2341 Ok(response.channels)
2342 }
2343
2344 async fn handle_payment_summary_msg(
2347 &self,
2348 PaymentSummaryPayload {
2349 start_millis,
2350 end_millis,
2351 }: PaymentSummaryPayload,
2352 ) -> AdminResult<PaymentSummaryResponse> {
2353 let federation_manager = self.federation_manager.read().await;
2354 let fed_configs = federation_manager.get_all_federation_configs().await;
2355 let federation_ids = fed_configs.keys().collect::<Vec<_>>();
2356 let start = UNIX_EPOCH + Duration::from_millis(start_millis);
2357 let end = UNIX_EPOCH + Duration::from_millis(end_millis);
2358
2359 if start > end {
2360 return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
2361 }
2362
2363 let mut outgoing = StructuredPaymentEvents::default();
2364 let mut incoming = StructuredPaymentEvents::default();
2365 for fed_id in federation_ids {
2366 let client = federation_manager
2367 .client(fed_id)
2368 .expect("No client available")
2369 .value();
2370 let all_events = &get_events_for_duration(client, start, end).await;
2371
2372 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
2373 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
2374 outgoing.combine(&mut lnv1_outgoing);
2375 incoming.combine(&mut lnv1_incoming);
2376 outgoing.combine(&mut lnv2_outgoing);
2377 incoming.combine(&mut lnv2_incoming);
2378 }
2379
2380 Ok(PaymentSummaryResponse {
2381 outgoing: PaymentStats::compute(&outgoing),
2382 incoming: PaymentStats::compute(&incoming),
2383 })
2384 }
2385
2386 async fn handle_leave_federation(
2391 &self,
2392 payload: LeaveFedPayload,
2393 ) -> AdminResult<FederationInfo> {
2394 let mut federation_manager = self.federation_manager.write().await;
2397 let mut dbtx = self.gateway_db.begin_transaction().await;
2398
2399 let federation_info = federation_manager
2400 .leave_federation(
2401 payload.federation_id,
2402 &mut dbtx.to_ref_nc(),
2403 self.registrations.values().collect(),
2404 )
2405 .await?;
2406
2407 dbtx.remove_federation_config(payload.federation_id).await;
2408 dbtx.commit_tx().await;
2409 self.registration_health
2410 .clear_federation(payload.federation_id)
2411 .await;
2412 Ok(federation_info)
2413 }
2414
2415 async fn handle_connect_federation(
2420 &self,
2421 payload: ConnectFedPayload,
2422 ) -> AdminResult<FederationInfo> {
2423 let GatewayState::Running { lightning_context } = self.get_state().await else {
2424 return Err(AdminGatewayError::Lightning(
2425 LightningRpcError::FailedToConnect,
2426 ));
2427 };
2428
2429 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
2430 AdminGatewayError::ClientCreationError(anyhow!(format!(
2431 "Invalid federation member string {e:?}"
2432 )))
2433 })?;
2434
2435 let federation_id = invite_code.federation_id();
2436
2437 let mut federation_manager = self.federation_manager.write().await;
2438
2439 if federation_manager.has_federation(federation_id) {
2441 return Err(AdminGatewayError::ClientCreationError(anyhow!(
2442 "Federation has already been registered"
2443 )));
2444 }
2445
2446 let federation_index = federation_manager.pop_next_index()?;
2449
2450 let federation_config = FederationConfig {
2451 invite_code,
2452 federation_index,
2453 lightning_fee: self.default_routing_fees,
2454 transaction_fee: self.default_transaction_fees,
2455 payment_policies: BTreeSet::new(),
2456 _connector: ConnectorType::Tcp,
2458 };
2459
2460 let routing_fees = RoutingFees::try_from(federation_config.lightning_fee)
2464 .map_err(|err| AdminGatewayError::GatewayConfigurationError(err.to_string()))?;
2465
2466 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2467 .await
2468 .expect("mnemonic should be set");
2469 let recover = payload.recover.unwrap_or(false);
2470 if recover {
2471 self.client_builder
2472 .recover(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2473 .await?;
2474 }
2475
2476 let client = self
2477 .client_builder
2478 .build(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2479 .await?;
2480
2481 if recover {
2482 client.wait_for_all_active_state_machines().await;
2483 }
2484
2485 let federation_info = FederationInfo {
2488 federation_id,
2489 federation_name: federation_manager.federation_name(&client).await,
2490 balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
2491 warn!(
2492 target: LOG_GATEWAY,
2493 err = %err.fmt_compact(),
2494 %federation_id,
2495 "Balance not immediately available after joining/recovering."
2496 );
2497 Amount::default()
2498 }),
2499 config: federation_config.clone(),
2500 last_backup_time: None,
2501 };
2502
2503 Self::check_federation_network(&client, self.network).await?;
2504 if matches!(self.lightning_mode, LightningMode::Lnd { .. })
2505 && let Ok(lnv1) = client.get_first_module::<GatewayClientModule>()
2506 {
2507 for (protocol, registration) in &self.registrations {
2508 let attempt = self
2509 .registration_health
2510 .begin_lnv1_attempt(federation_id, protocol.clone());
2511 let succeeded = lnv1
2512 .try_register_with_federation(
2513 Vec::new(),
2515 GW_ANNOUNCEMENT_TTL,
2516 routing_fees,
2517 lightning_context.clone(),
2518 registration.endpoint_url.clone(),
2519 registration.keypair,
2520 )
2521 .await;
2522 self.registration_health
2523 .complete_attempt(
2524 attempt,
2525 succeeded,
2526 fedimint_core::time::now(),
2527 fedimint_core::runtime::Instant::now(),
2528 )
2529 .await;
2530 }
2531 }
2532
2533 federation_manager.add_client(
2535 federation_index,
2536 Spanned::new(
2537 info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
2538 async { client },
2539 )
2540 .await,
2541 );
2542
2543 let mut dbtx = self.gateway_db.begin_transaction().await;
2544 dbtx.save_federation_config(&federation_config).await;
2545 dbtx.save_federation_backup_record(federation_id, None)
2546 .await;
2547 dbtx.commit_tx().await;
2548 debug!(
2549 target: LOG_GATEWAY,
2550 federation_id = %federation_id,
2551 federation_index = %federation_index,
2552 "Federation connected"
2553 );
2554
2555 Ok(federation_info)
2556 }
2557
2558 async fn handle_set_fees_msg(
2561 &self,
2562 SetFeesPayload {
2563 federation_id,
2564 lightning_base,
2565 lightning_parts_per_million,
2566 transaction_base,
2567 transaction_parts_per_million,
2568 }: SetFeesPayload,
2569 ) -> AdminResult<()> {
2570 let mut dbtx = self.gateway_db.begin_transaction().await;
2571 let mut fed_configs = if let Some(fed_id) = federation_id {
2572 dbtx.load_federation_configs()
2573 .await
2574 .into_iter()
2575 .filter(|(id, _)| *id == fed_id)
2576 .collect::<BTreeMap<_, _>>()
2577 } else {
2578 dbtx.load_federation_configs().await
2579 };
2580
2581 let federation_manager = self.federation_manager.read().await;
2582
2583 for (federation_id, config) in &mut fed_configs {
2584 let mut lightning_fee = config.lightning_fee;
2585 if let Some(lightning_base) = lightning_base {
2586 lightning_fee.base = lightning_base;
2587 }
2588
2589 if let Some(lightning_ppm) = lightning_parts_per_million {
2590 lightning_fee.parts_per_million = lightning_ppm;
2591 }
2592
2593 let mut transaction_fee = config.transaction_fee;
2594 if let Some(transaction_base) = transaction_base {
2595 transaction_fee.base = transaction_base;
2596 }
2597
2598 if let Some(transaction_ppm) = transaction_parts_per_million {
2599 transaction_fee.parts_per_million = transaction_ppm;
2600 }
2601
2602 federation_manager
2605 .client(federation_id)
2606 .ok_or(FederationNotConnected {
2607 federation_id_prefix: federation_id.to_prefix(),
2608 })?;
2609
2610 let send_fees = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
2616 AdminGatewayError::GatewayConfigurationError(format!(
2617 "Total Send fees overflowed, they may not exceed {}",
2618 PaymentFee::SEND_FEE_LIMIT
2619 ))
2620 })?;
2621
2622 if !send_fees.is_within(&PaymentFee::SEND_FEE_LIMIT) {
2624 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2625 "Total Send fees exceeded {}",
2626 PaymentFee::SEND_FEE_LIMIT
2627 )));
2628 }
2629
2630 if !transaction_fee.is_within(&PaymentFee::RECEIVE_FEE_LIMIT) {
2632 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2633 "Transaction fees exceeded RECEIVE LIMIT {}",
2634 PaymentFee::RECEIVE_FEE_LIMIT
2635 )));
2636 }
2637
2638 config.lightning_fee = lightning_fee;
2639 config.transaction_fee = transaction_fee;
2640 dbtx.save_federation_config(config).await;
2641 }
2642
2643 dbtx.commit_tx().await;
2644
2645 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2646 let register_task_group = TaskGroup::new();
2647
2648 self.register_federations(&fed_configs, ®ister_task_group)
2649 .await;
2650 }
2651
2652 Ok(())
2653 }
2654
2655 async fn handle_set_payment_policy_msg(
2660 &self,
2661 SetPaymentPolicyPayload {
2662 federation_id,
2663 receive_enabled,
2664 }: SetPaymentPolicyPayload,
2665 ) -> AdminResult<()> {
2666 let mut dbtx = self.gateway_db.begin_transaction().await;
2667 let mut fed_configs = if let Some(fed_id) = federation_id {
2668 dbtx.load_federation_configs()
2669 .await
2670 .into_iter()
2671 .filter(|(id, _)| *id == fed_id)
2672 .collect::<BTreeMap<_, _>>()
2673 } else {
2674 dbtx.load_federation_configs().await
2675 };
2676
2677 if let Some(fed_id) = federation_id
2680 && fed_configs.is_empty()
2681 {
2682 return Err(FederationNotConnected {
2683 federation_id_prefix: fed_id.to_prefix(),
2684 }
2685 .into());
2686 }
2687
2688 let federation_manager = self.federation_manager.read().await;
2689
2690 for (federation_id, config) in &mut fed_configs {
2691 federation_manager
2692 .client(federation_id)
2693 .ok_or(FederationNotConnected {
2694 federation_id_prefix: federation_id.to_prefix(),
2695 })?;
2696
2697 if let Some(receive_enabled) = receive_enabled {
2698 if receive_enabled {
2699 config
2700 .payment_policies
2701 .remove(&PaymentPolicy::ReceivesDisabled);
2702 } else {
2703 config
2704 .payment_policies
2705 .insert(PaymentPolicy::ReceivesDisabled);
2706 }
2707 }
2708
2709 info!(
2710 target: LOG_GATEWAY,
2711 %federation_id,
2712 payment_policies = ?config.payment_policies,
2713 "Updated federation payment policy"
2714 );
2715
2716 dbtx.save_federation_config(config).await;
2717 }
2718
2719 dbtx.commit_tx().await;
2720
2721 Ok(())
2722 }
2723
2724 async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
2728 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2729 .await
2730 .expect("mnemonic should be set");
2731 let words = mnemonic
2732 .words()
2733 .map(std::string::ToString::to_string)
2734 .collect::<Vec<_>>();
2735 let all_federations = self
2736 .federation_manager
2737 .read()
2738 .await
2739 .get_all_federation_configs()
2740 .await
2741 .keys()
2742 .copied()
2743 .collect::<BTreeSet<_>>();
2744 let legacy_federations = self.client_builder.legacy_federations(all_federations);
2745 let mnemonic_response = MnemonicResponse {
2746 mnemonic: words,
2747 legacy_federations,
2748 };
2749 Ok(mnemonic_response)
2750 }
2751
2752 async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
2755 info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
2756 let context = self.get_lightning_context().await?;
2757 let res = context.lnrpc.open_channel(payload).await?;
2758 info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
2759 Txid::from_str(&res.funding_txid).map_err(|e| {
2760 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2761 failure_reason: format!("Received invalid channel funding txid string {e}"),
2762 })
2763 })
2764 }
2765
2766 async fn handle_connect_peer_msg(&self, payload: ConnectPeerRequest) -> AdminResult<()> {
2769 info!(
2770 target: LOG_GATEWAY,
2771 pubkey = %payload.node_address.pubkey,
2772 host = %payload.node_address.host_with_port(),
2773 "Connecting to Lightning peer..."
2774 );
2775 let context = self.get_lightning_context().await?;
2776 context.lnrpc.connect_peer(payload).await?;
2777 info!(target: LOG_GATEWAY, "Connected to Lightning peer");
2778 Ok(())
2779 }
2780
2781 async fn handle_close_channels_with_peer_msg(
2784 &self,
2785 payload: CloseChannelsWithPeerRequest,
2786 ) -> AdminResult<CloseChannelsWithPeerResponse> {
2787 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Closing lightning channel...");
2788 let context = self.get_lightning_context().await?;
2789 let response = context
2790 .lnrpc
2791 .close_channels_with_peer(payload.clone())
2792 .await?;
2793 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Initiated channel closure");
2794 Ok(response)
2795 }
2796
2797 async fn handle_set_channel_fees_msg(&self, payload: SetChannelFeesRequest) -> AdminResult<()> {
2800 info!(
2801 target: LOG_GATEWAY,
2802 funding_outpoint = %payload.funding_outpoint,
2803 base_fee_msat = payload.base_fee_msat,
2804 parts_per_million = payload.parts_per_million,
2805 "Updating channel fees..."
2806 );
2807 let context = self.get_lightning_context().await?;
2808 context.lnrpc.set_channel_fees(payload).await?;
2809 Ok(())
2810 }
2811
2812 async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
2815 let dbtx = self.gateway_db.begin_transaction_nc().await;
2816 let federation_infos = self
2817 .federation_manager
2818 .read()
2819 .await
2820 .federation_info_all_federations(dbtx)
2821 .await;
2822
2823 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
2824 .iter()
2825 .map(|federation_info| FederationBalanceInfo {
2826 federation_id: federation_info.federation_id,
2827 ecash_balance_msats: Amount {
2828 msats: federation_info.balance_msat.msats,
2829 },
2830 })
2831 .collect();
2832
2833 let context = self.get_lightning_context().await?;
2834 let lightning_node_balances = context.lnrpc.get_balances().await?;
2835
2836 Ok(GatewayBalances {
2837 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
2838 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
2839 ecash_balances,
2840 inbound_lightning_liquidity_msats: lightning_node_balances
2841 .inbound_lightning_liquidity_msats,
2842 })
2843 }
2844
2845 async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
2847 let context = self.get_lightning_context().await?;
2848 let response = context.lnrpc.send_onchain(payload.clone()).await?;
2849 let txid =
2850 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
2851 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
2852 })?;
2853 info!(onchain_request = %payload, txid = %txid, "Sent onchain transaction");
2854 Ok(txid)
2855 }
2856
2857 async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
2859 let context = self.get_lightning_context().await?;
2860 let response = context.lnrpc.get_ln_onchain_address().await?;
2861
2862 let address = Address::from_str(&response.address).map_err(|e| {
2863 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2864 failure_reason: e.to_string(),
2865 })
2866 })?;
2867
2868 address.require_network(self.network).map_err(|e| {
2869 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2870 failure_reason: e.to_string(),
2871 })
2872 })
2873 }
2874
2875 async fn handle_deposit_address_msg(
2876 &self,
2877 payload: DepositAddressPayload,
2878 ) -> AdminResult<Address> {
2879 self.handle_address_msg(payload).await
2880 }
2881
2882 async fn handle_receive_ecash_msg(
2883 &self,
2884 payload: ReceiveEcashPayload,
2885 ) -> AdminResult<ReceiveEcashResponse> {
2886 Self::handle_receive_ecash_msg(self, payload)
2887 .await
2888 .map_err(|e| AdminGatewayError::Unexpected(anyhow::anyhow!("{e}")))
2889 }
2890
2891 async fn handle_create_invoice_for_operator_msg(
2894 &self,
2895 payload: CreateInvoiceForOperatorPayload,
2896 ) -> AdminResult<Bolt11Invoice> {
2897 let GatewayState::Running { lightning_context } = self.get_state().await else {
2898 return Err(AdminGatewayError::Lightning(
2899 LightningRpcError::FailedToConnect,
2900 ));
2901 };
2902
2903 Bolt11Invoice::from_str(
2904 &lightning_context
2905 .lnrpc
2906 .create_invoice(CreateInvoiceRequest {
2907 payment_hash: None, amount_msat: payload.amount_msats,
2910 expiry_secs: payload.expiry_secs.unwrap_or(3600),
2911 description: payload.description.map(InvoiceDescription::Direct),
2912 })
2913 .await?
2914 .invoice,
2915 )
2916 .map_err(|e| {
2917 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2918 failure_reason: e.to_string(),
2919 })
2920 })
2921 }
2922
2923 async fn handle_pay_invoice_for_operator_msg(
2926 &self,
2927 payload: PayInvoiceForOperatorPayload,
2928 ) -> AdminResult<Preimage> {
2929 const BASE_FEE: u64 = 50;
2931 const FEE_DENOMINATOR: u64 = 100;
2932 const MAX_DELAY: u64 = 1008;
2933
2934 let GatewayState::Running { lightning_context } = self.get_state().await else {
2935 return Err(AdminGatewayError::Lightning(
2936 LightningRpcError::FailedToConnect,
2937 ));
2938 };
2939
2940 let max_fee = BASE_FEE
2941 + payload
2942 .invoice
2943 .amount_milli_satoshis()
2944 .context("Invoice is missing amount")?
2945 .saturating_div(FEE_DENOMINATOR);
2946
2947 let res = lightning_context
2948 .lnrpc
2949 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
2950 .await?;
2951 Ok(res.preimage)
2952 }
2953
2954 async fn handle_list_transactions_msg(
2956 &self,
2957 payload: ListTransactionsPayload,
2958 ) -> AdminResult<ListTransactionsResponse> {
2959 let lightning_context = self.get_lightning_context().await?;
2960 let response = lightning_context
2961 .lnrpc
2962 .list_transactions(payload.start_secs, payload.end_secs)
2963 .await?;
2964 Ok(response)
2965 }
2966
2967 async fn handle_spend_ecash_msg(
2969 &self,
2970 payload: SpendEcashPayload,
2971 ) -> AdminResult<SpendEcashResponse> {
2972 let client = self
2973 .select_client(payload.federation_id)
2974 .await?
2975 .into_value();
2976
2977 if let Ok(mint_module) = client.get_first_module::<MintClientModule>() {
2978 let notes = mint_module
2979 .send_oob_notes(payload.amount, ())
2980 .await
2981 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
2982 debug!(target: LOG_GATEWAY, ?notes, "Spend ecash notes");
2983 Ok(SpendEcashResponse {
2984 notes: notes.to_string(),
2985 })
2986 } else if let Ok(mint_module) = client.get_primary_module_for_unit::<MintV2ClientModule>(
2987 fedimint_core::module::AmountUnit::BITCOIN,
2988 ) {
2989 let (_, ecash) = mint_module
2990 .send(payload.amount, serde_json::Value::Null, true)
2991 .await
2992 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
2993
2994 Ok(SpendEcashResponse {
2995 notes: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
2996 })
2997 } else {
2998 Err(AdminGatewayError::Unexpected(anyhow::anyhow!(
2999 "No mint module available"
3000 )))
3001 }
3002 }
3003
3004 async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
3007 let was_running = {
3011 let mut state_guard = self.state.write().await;
3012 if let GatewayState::Running { lightning_context } = state_guard.clone() {
3013 *state_guard = GatewayState::ShuttingDown { lightning_context };
3014 true
3015 } else {
3016 false
3017 }
3018 };
3019
3020 if was_running {
3028 self.federation_manager
3029 .read()
3030 .await
3031 .wait_for_incoming_payments()
3032 .await?;
3033 }
3034
3035 let tg = task_group.clone();
3036 tg.spawn("Kill Gateway", |_task_handle| async {
3037 if let Err(err) = task_group.shutdown_join_all(Duration::from_mins(3)).await {
3038 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error shutting down gateway");
3039 }
3040 });
3041 Ok(())
3042 }
3043
3044 fn get_task_group(&self) -> TaskGroup {
3045 self.task_group.clone()
3046 }
3047
3048 async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> AdminResult<WithdrawResponse> {
3051 let WithdrawPayload {
3052 amount,
3053 address,
3054 federation_id,
3055 quoted_fees,
3056 } = payload;
3057
3058 let address_network = get_network_for_address(&address);
3059 let gateway_network = self.network;
3060 let Ok(address) = address.require_network(gateway_network) else {
3061 return Err(AdminGatewayError::WithdrawError {
3062 failure_reason: format!(
3063 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
3064 ),
3065 });
3066 };
3067
3068 let client = self.select_client(federation_id).await?;
3069
3070 if let Ok(wallet_module) = client
3071 .value()
3072 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
3073 {
3074 return withdraw_v2(client.value(), &wallet_module, &address, amount).await;
3075 }
3076
3077 let wallet_module = client
3078 .value()
3079 .get_first_module::<WalletClientModule>()
3080 .map_err(anyhow::Error::from)?;
3081
3082 let (withdraw_amount, fees) = match quoted_fees {
3085 Some(fees) => {
3087 let amt = match amount {
3088 BitcoinAmountOrAll::Amount(a) => a,
3089 BitcoinAmountOrAll::All => {
3090 return Err(AdminGatewayError::WithdrawError {
3092 failure_reason:
3093 "Cannot use 'all' with quoted fees - amount must be resolved first"
3094 .to_string(),
3095 });
3096 }
3097 };
3098 (amt, fees)
3099 }
3100 None => match amount {
3102 BitcoinAmountOrAll::All => {
3107 let balance = client.value().get_balance_for_btc().await.map_err(|err| {
3108 AdminGatewayError::Unexpected(anyhow!(
3109 "Balance not available: {}",
3110 err.fmt_compact()
3111 ))
3112 })?;
3113
3114 wallet_module
3115 .max_withdrawable_amount(&address, balance)
3116 .await
3117 .map_err(|err| AdminGatewayError::WithdrawError {
3118 failure_reason: format!(
3119 "Insufficient funds. Balance: {balance}: {}",
3120 err.fmt_compact()
3121 ),
3122 })?
3123 }
3124 BitcoinAmountOrAll::Amount(amount) => (
3125 amount,
3126 wallet_module
3127 .get_withdraw_fees(&address, amount)
3128 .await
3129 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?,
3130 ),
3131 },
3132 };
3133
3134 let operation_id = wallet_module
3135 .withdraw(&address, withdraw_amount, fees, ())
3136 .await
3137 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
3138 let mut updates = wallet_module
3139 .subscribe_withdraw_updates(operation_id)
3140 .await
3141 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?
3142 .into_stream();
3143
3144 while let Some(update) = updates.next().await {
3145 match update {
3146 WithdrawState::Succeeded(txid) => {
3147 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds");
3148 return Ok(WithdrawResponse { txid, fees });
3149 }
3150 WithdrawState::Failed(e) => {
3151 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
3152 }
3153 WithdrawState::Created => {}
3154 }
3155 }
3156
3157 Err(AdminGatewayError::WithdrawError {
3158 failure_reason: "Ran out of state updates while withdrawing".to_string(),
3159 })
3160 }
3161
3162 async fn handle_withdraw_preview_msg(
3165 &self,
3166 payload: WithdrawPreviewPayload,
3167 ) -> AdminResult<WithdrawPreviewResponse> {
3168 let gateway_network = self.network;
3169 let address_checked = payload
3170 .address
3171 .clone()
3172 .require_network(gateway_network)
3173 .map_err(|_| AdminGatewayError::WithdrawError {
3174 failure_reason: "Address network mismatch".to_string(),
3175 })?;
3176
3177 let client = self.select_client(payload.federation_id).await?;
3178
3179 let WithdrawDetails {
3180 amount,
3181 mint_fees,
3182 peg_out_fees,
3183 } = match payload.amount {
3184 BitcoinAmountOrAll::All => {
3185 calculate_max_withdrawable(client.value(), &address_checked).await?
3186 }
3187 BitcoinAmountOrAll::Amount(btc_amount) => {
3188 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
3189 WithdrawDetails {
3190 amount: btc_amount.into(),
3191 mint_fees: None,
3192 peg_out_fees: wallet_module
3193 .get_withdraw_fees(&address_checked, btc_amount)
3194 .await
3195 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?,
3196 }
3197 } else if let Ok(wallet_module) = client
3198 .value()
3199 .get_first_module::<fedimint_walletv2_client::WalletClientModule>(
3200 ) {
3201 let fee = wallet_module.send_fee().await.map_err(|e| {
3202 AdminGatewayError::WithdrawError {
3203 failure_reason: e.fmt_compact().to_string(),
3204 }
3205 })?;
3206 WithdrawDetails {
3207 amount: btc_amount.into(),
3208 mint_fees: None,
3209 peg_out_fees: PegOutFees::from_amount(fee),
3210 }
3211 } else {
3212 return Err(AdminGatewayError::Unexpected(anyhow!(
3213 "No wallet module found"
3214 )));
3215 }
3216 }
3217 };
3218
3219 let total_cost = amount
3220 .checked_add(peg_out_fees.amount().into())
3221 .and_then(|a| a.checked_add(mint_fees.unwrap_or(Amount::ZERO)))
3222 .ok_or_else(|| AdminGatewayError::Unexpected(anyhow!("Total cost overflow")))?;
3223
3224 Ok(WithdrawPreviewResponse {
3225 withdraw_amount: amount,
3226 address: payload.address.assume_checked().to_string(),
3227 peg_out_fees,
3228 total_cost,
3229 mint_fees,
3230 })
3231 }
3232
3233 async fn handle_payment_log_msg(
3245 &self,
3246 PaymentLogPayload {
3247 end_position,
3248 pagination_size,
3249 federation_id,
3250 event_kinds,
3251 }: PaymentLogPayload,
3252 ) -> AdminResult<PaymentLogResponse> {
3253 const BATCH_SIZE: u64 = 10_000;
3254 let federation_manager = self.federation_manager.read().await;
3255 let client = federation_manager
3256 .client(&federation_id)
3257 .ok_or(FederationNotConnected {
3258 federation_id_prefix: federation_id.to_prefix(),
3259 })?
3260 .value();
3261
3262 let event_kinds = if event_kinds.is_empty() {
3266 ALL_GATEWAY_EVENTS.to_vec()
3267 } else {
3268 event_kinds
3269 };
3270
3271 let end_position = if let Some(position) = end_position {
3272 position
3273 } else {
3274 let mut dbtx = client.db().begin_transaction_nc().await;
3275 dbtx.get_next_event_log_id().await
3276 };
3277
3278 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
3279
3280 let mut payment_log = Vec::new();
3281
3282 while payment_log.len() < pagination_size {
3283 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
3284 let mut filtered_batch = batch
3285 .into_iter()
3286 .filter(|e| e.id() <= end_position && event_kinds.contains(&e.as_raw().kind))
3287 .collect::<Vec<_>>();
3288 filtered_batch.reverse();
3289 payment_log.extend(filtered_batch);
3290
3291 start_position = start_position.saturating_sub(BATCH_SIZE);
3293
3294 if start_position == EventLogId::LOG_START {
3295 break;
3296 }
3297 }
3298
3299 payment_log.truncate(pagination_size);
3301
3302 Ok(PaymentLogResponse(payment_log))
3303 }
3304
3305 async fn handle_set_mnemonic_msg(&self, payload: SetMnemonicPayload) -> AdminResult<()> {
3308 let mut state_guard = self.state.write().await;
3313
3314 let GatewayState::NotConfigured { mnemonic_sender } = state_guard.clone() else {
3316 return Err(AdminGatewayError::MnemonicError(anyhow!(
3317 "Gateway is not is NotConfigured state"
3318 )));
3319 };
3320
3321 let mnemonic = if let Some(words) = payload.words {
3322 info!(target: LOG_GATEWAY, "Using user provided mnemonic");
3323 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(|e| {
3324 AdminGatewayError::MnemonicError(anyhow!(format!(
3325 "Seed phrase provided in environment was invalid {e:?}"
3326 )))
3327 })?
3328 } else {
3329 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
3330 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
3331 };
3332
3333 Client::store_encodable_client_secret(&self.gateway_db, mnemonic.to_entropy())
3334 .await
3335 .map_err(|err| AdminGatewayError::MnemonicError(err.into()))?;
3336
3337 *state_guard = GatewayState::Disconnected;
3338 drop(state_guard);
3339
3340 let _ = mnemonic_sender.send(());
3342
3343 Ok(())
3344 }
3345
3346 async fn handle_create_offer_for_operator_msg(
3348 &self,
3349 payload: CreateOfferPayload,
3350 ) -> AdminResult<CreateOfferResponse> {
3351 let lightning_context = self.get_lightning_context().await?;
3352 let offer = lightning_context.lnrpc.create_offer(
3353 payload.amount,
3354 payload.description,
3355 payload.expiry_secs,
3356 payload.quantity,
3357 )?;
3358 Ok(CreateOfferResponse { offer })
3359 }
3360
3361 async fn handle_pay_offer_for_operator_msg(
3363 &self,
3364 payload: PayOfferPayload,
3365 ) -> AdminResult<PayOfferResponse> {
3366 let lightning_context = self.get_lightning_context().await?;
3367 let preimage = lightning_context
3368 .lnrpc
3369 .pay_offer(
3370 payload.offer,
3371 payload.quantity,
3372 payload.amount,
3373 payload.payer_note,
3374 )
3375 .await?;
3376 Ok(PayOfferResponse {
3377 preimage: preimage.to_string(),
3378 })
3379 }
3380
3381 async fn handle_export_invite_codes(
3384 &self,
3385 ) -> BTreeMap<FederationId, BTreeMap<PeerId, (String, InviteCode)>> {
3386 let fed_manager = self.federation_manager.read().await;
3387 fed_manager.all_invite_codes().await
3388 }
3389
3390 async fn handle_get_note_summary_msg(
3393 &self,
3394 federation_id: &FederationId,
3395 ) -> AdminResult<TieredCounts> {
3396 let fed_manager = self.federation_manager.read().await;
3397 fed_manager.get_note_summary(federation_id).await
3398 }
3399
3400 fn get_password_hash(&self) -> String {
3401 self.bcrypt_password_hash.clone()
3402 }
3403
3404 fn gatewayd_version(&self) -> String {
3405 let gatewayd_version = env!("CARGO_PKG_VERSION");
3406 gatewayd_version.to_string()
3407 }
3408
3409 async fn get_chain_source(&self) -> (ChainSource, Network) {
3410 (self.chain_source.clone(), self.network)
3411 }
3412
3413 fn lightning_mode(&self) -> LightningMode {
3414 self.lightning_mode.clone()
3415 }
3416
3417 async fn is_configured(&self) -> bool {
3418 !matches!(self.get_state().await, GatewayState::NotConfigured { .. })
3419 }
3420}
3421
3422impl Gateway {
3424 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
3428 self.federation_manager
3429 .read()
3430 .await
3431 .client(federation_id)
3432 .and_then(|client| {
3433 client
3436 .value()
3437 .get_first_module::<GatewayClientModuleV2>()
3438 .ok()
3439 .map(|module| module.keypair.public_key())
3440 })
3441 }
3442
3443 async fn receive_enabled(&self, federation_id: FederationId) -> bool {
3448 self.gateway_db
3449 .begin_transaction_nc()
3450 .await
3451 .load_federation_config(federation_id)
3452 .await
3453 .is_none_or(|config| config.receive_enabled())
3454 }
3455
3456 pub async fn routing_info_v2(
3459 &self,
3460 federation_id: &FederationId,
3461 ) -> Result<Option<RoutingInfo>> {
3462 let context = self.get_lightning_context().await?;
3463
3464 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
3465 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
3466 PublicGatewayError::FederationNotConnected(FederationNotConnected {
3467 federation_id_prefix: federation_id.to_prefix(),
3468 }),
3469 )?;
3470
3471 let lightning_fee = fed_config.lightning_fee;
3472 let transaction_fee = fed_config.transaction_fee;
3473 let receive_enabled = fed_config.receive_enabled();
3474
3475 let send_fee_default = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
3478 PublicGatewayError::Unexpected(anyhow!(
3479 "The configured fees of federation {federation_id} cannot be added"
3480 ))
3481 })?;
3482
3483 Ok(self
3484 .public_key_v2(federation_id)
3485 .await
3486 .map(|module_public_key| RoutingInfo {
3487 lightning_public_key: context.lightning_public_key,
3488 lightning_alias: Some(context.lightning_alias.clone()),
3489 module_public_key,
3490 send_fee_default,
3491 send_fee_minimum: transaction_fee,
3495 expiration_delta_default: 1440,
3496 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
3497 receive_fee: transaction_fee,
3500 receive_enabled,
3501 }))
3502 }
3503
3504 pub async fn send_payment_v2(
3507 &self,
3508 payload: SendPaymentPayload,
3509 ) -> Result<std::result::Result<[u8; 32], Signature>> {
3510 let client = self.select_client(payload.federation_id).await?;
3511 let module = client
3514 .value()
3515 .get_first_module::<GatewayClientModuleV2>()
3516 .map_err(|err| PublicGatewayError::LNv2(LNv2Error::OutgoingPayment(err.into())))?;
3517
3518 module
3519 .send_payment(payload)
3520 .await
3521 .map_err(|err| LNv2Error::OutgoingPayment(err.into()))
3522 .map_err(PublicGatewayError::LNv2)
3523 }
3524
3525 pub async fn create_bolt11_invoice_v2(
3530 &self,
3531 payload: CreateBolt11InvoicePayload,
3532 ) -> Result<Bolt11Invoice> {
3533 if !self.invoice_rate_limiter.try_acquire() {
3537 return Err(PublicGatewayError::RateLimited);
3538 }
3539
3540 if !payload.contract.verify() {
3541 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3542 "The contract is invalid".to_string(),
3543 )));
3544 }
3545
3546 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
3547 LNv2Error::IncomingPayment(format!(
3548 "Federation {} does not exist",
3549 payload.federation_id
3550 )),
3551 )?;
3552
3553 if !payment_info.receive_enabled {
3554 return Err(PublicGatewayError::ReceiveDisabled {
3555 federation_id_prefix: payload.federation_id.to_prefix(),
3556 });
3557 }
3558
3559 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
3560 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3561 "The incoming contract is keyed to another gateway".to_string(),
3562 )));
3563 }
3564
3565 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
3566
3567 if contract_amount == Amount::ZERO {
3568 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3569 "Zero amount incoming contracts are not supported".to_string(),
3570 )));
3571 }
3572
3573 if contract_amount != payload.contract.commitment.amount {
3574 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3575 "The contract amount does not pay the correct amount of fees".to_string(),
3576 )));
3577 }
3578
3579 if payload.contract.commitment.expiration_or_fee <= duration_since_epoch().as_secs() {
3580 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3581 "The contract has already expired".to_string(),
3582 )));
3583 }
3584
3585 if payload.expiry_secs > MAX_INVOICE_EXPIRY_SECS {
3586 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3587 "The invoice expiry exceeds the maximum of one day".to_string(),
3588 )));
3589 }
3590
3591 let payment_hash = match payload.contract.commitment.payment_image {
3592 PaymentImage::Hash(payment_hash) => payment_hash,
3593 PaymentImage::Point(..) => {
3594 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3595 "PaymentImage is not a payment hash".to_string(),
3596 )));
3597 }
3598 };
3599
3600 let mut dbtx = self.gateway_db.begin_transaction().await;
3605
3606 let invoice_expires_at_secs = duration_since_epoch()
3607 .as_secs()
3608 .saturating_add(u64::from(payload.expiry_secs));
3609
3610 if dbtx
3611 .save_registered_incoming_contract(
3612 payload.federation_id,
3613 payload.amount,
3614 invoice_expires_at_secs,
3615 payload.contract,
3616 )
3617 .await
3618 .is_some()
3619 {
3620 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3621 "PaymentHash is already registered".to_string(),
3622 )));
3623 }
3624
3625 dbtx.commit_tx_result().await.map_err(|_| {
3626 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3627 "Payment hash is already registered".to_string(),
3628 ))
3629 })?;
3630
3631 match self
3632 .create_invoice_via_lnrpc_v2(
3633 payment_hash,
3634 payload.amount,
3635 payload.description.clone(),
3636 payload.expiry_secs,
3637 )
3638 .await
3639 {
3640 Ok(invoice) => Ok(invoice),
3641 Err(err) => {
3642 let mut dbtx = self.gateway_db.begin_transaction().await;
3645 dbtx.delete_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3646 .await;
3647 if let Err(db_err) = dbtx.commit_tx_result().await {
3648 warn!(
3649 target: LOG_GATEWAY,
3650 err = %db_err.fmt_compact(),
3651 %payment_hash,
3652 "Failed to release incoming contract reservation after Lightning error"
3653 );
3654 }
3655
3656 Err(err.into())
3657 }
3658 }
3659 }
3660
3661 pub async fn create_invoice_via_lnrpc_v2(
3664 &self,
3665 payment_hash: sha256::Hash,
3666 amount: Amount,
3667 description: Bolt11InvoiceDescription,
3668 expiry_time: u32,
3669 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
3670 let lnrpc = self.get_lightning_context().await?.lnrpc;
3671
3672 let response = match description {
3673 Bolt11InvoiceDescription::Direct(description) => {
3674 lnrpc
3675 .create_invoice(CreateInvoiceRequest {
3676 payment_hash: Some(payment_hash),
3677 amount_msat: amount.msats,
3678 expiry_secs: expiry_time,
3679 description: Some(InvoiceDescription::Direct(description)),
3680 })
3681 .await?
3682 }
3683 Bolt11InvoiceDescription::Hash(hash) => {
3684 lnrpc
3685 .create_invoice(CreateInvoiceRequest {
3686 payment_hash: Some(payment_hash),
3687 amount_msat: amount.msats,
3688 expiry_secs: expiry_time,
3689 description: Some(InvoiceDescription::Hash(hash)),
3690 })
3691 .await?
3692 }
3693 };
3694
3695 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
3696 LightningRpcError::FailedToGetInvoice {
3697 failure_reason: e.to_string(),
3698 }
3699 })
3700 }
3701
3702 pub async fn verify_bolt11_preimage_v2(
3703 &self,
3704 payment_hash: sha256::Hash,
3705 wait: bool,
3706 ) -> std::result::Result<VerifyResponse, String> {
3707 let registered_contract = self
3708 .gateway_db
3709 .begin_transaction_nc()
3710 .await
3711 .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3712 .await
3713 .ok_or("Unknown payment hash".to_string())?;
3714
3715 let client = self
3716 .select_client(registered_contract.federation_id)
3717 .await
3718 .map_err(|_| "Not connected to federation".to_string())?
3719 .into_value();
3720
3721 let operation_id = OperationId::from_encodable(®istered_contract.contract);
3722
3723 if !(wait || client.operation_exists(operation_id).await) {
3724 return Ok(VerifyResponse {
3725 settled: false,
3726 preimage: None,
3727 });
3728 }
3729
3730 let module = client
3731 .get_first_module::<GatewayClientModuleV2>()
3732 .expect("Must have client module");
3733
3734 let Ok(state) = timeout(VERIFY_WAIT_TIMEOUT, module.await_receive(operation_id)).await
3735 else {
3736 return Ok(VerifyResponse {
3737 settled: false,
3738 preimage: None,
3739 });
3740 };
3741
3742 let preimage = match state {
3743 FinalReceiveState::Success(preimage) => Ok(preimage),
3744 FinalReceiveState::Failure => Err("Payment has failed".to_string()),
3745 FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
3746 FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
3747 }?;
3748
3749 Ok(VerifyResponse {
3750 settled: true,
3751 preimage: Some(preimage),
3752 })
3753 }
3754
3755 pub async fn get_registered_incoming_contract_and_client_v2(
3759 &self,
3760 payment_image: PaymentImage,
3761 amount_msats: u64,
3762 ) -> Result<(IncomingContract, ClientHandleArc)> {
3763 let registered_incoming_contract = self
3764 .gateway_db
3765 .begin_transaction_nc()
3766 .await
3767 .load_registered_incoming_contract(payment_image)
3768 .await
3769 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3770 "No corresponding decryption contract available".to_string(),
3771 )))?;
3772
3773 if registered_incoming_contract.incoming_amount_msats != amount_msats {
3774 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3775 "The available decryption contract's amount is not equal to the requested amount"
3776 .to_string(),
3777 )));
3778 }
3779
3780 if !self
3784 .receive_enabled(registered_incoming_contract.federation_id)
3785 .await
3786 {
3787 return Err(PublicGatewayError::ReceiveDisabled {
3788 federation_id_prefix: registered_incoming_contract.federation_id.to_prefix(),
3789 });
3790 }
3791
3792 let client = self
3793 .select_client(registered_incoming_contract.federation_id)
3794 .await?
3795 .into_value();
3796
3797 Ok((registered_incoming_contract.contract, client))
3798 }
3799
3800 async fn await_complete_htlc(
3809 &self,
3810 htlc: InterceptPaymentResponse,
3811 ) -> std::result::Result<(), LightningRpcError> {
3812 loop {
3813 let lightning_context = self.await_lightning_context().await;
3814
3815 match lightning_context.lnrpc.complete_htlc(htlc.clone()).await {
3816 Ok(()) => return Ok(()),
3817 Err(err @ LightningRpcError::HtlcCompletionRejected { .. }) => {
3818 warn!(
3819 target: LOG_GATEWAY,
3820 err = %err.fmt_compact(),
3821 "Lightning cannot reach the requested terminal HTLC outcome",
3822 );
3823 return Err(err);
3824 }
3825 Err(err) => {
3826 warn!(
3827 target: LOG_GATEWAY,
3828 err = %err.fmt_compact(),
3829 "Failure trying to complete HTLC, retrying",
3830 );
3831 }
3832 }
3833
3834 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
3835 }
3836 }
3837
3838 async fn await_outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
3847 loop {
3848 let lightning_context = self.await_lightning_context().await;
3849
3850 match lightning_context
3851 .lnrpc
3852 .outbound_payment_exists(payment_hash)
3853 .await
3854 {
3855 Ok(exists) => return exists,
3856 Err(err) => {
3857 warn!(
3858 target: LOG_GATEWAY,
3859 err = %err.fmt_compact(),
3860 %payment_hash,
3861 "Failed to check for a dispatched payment, retrying",
3862 );
3863 }
3864 }
3865
3866 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
3867 }
3868 }
3869}
3870
3871#[async_trait]
3872impl IGatewayClientV2 for Gateway {
3873 async fn complete_htlc(
3874 &self,
3875 htlc_response: InterceptPaymentResponse,
3876 ) -> std::result::Result<(), LightningRpcError> {
3877 self.await_complete_htlc(htlc_response).await
3878 }
3879
3880 async fn is_direct_swap(
3881 &self,
3882 invoice: &Bolt11Invoice,
3883 ) -> std::result::Result<Option<(IncomingContract, ClientHandleArc)>, GatewayClientV2Error>
3884 {
3885 let lightning_context = self.await_lightning_context().await;
3890 if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
3891 let (contract, client) = self
3892 .get_registered_incoming_contract_and_client_v2(
3893 PaymentImage::Hash(*invoice.payment_hash()),
3894 invoice
3895 .amount_milli_satoshis()
3896 .expect("The amount invoice has been previously checked"),
3897 )
3898 .await
3899 .map_err(GatewayClientV2Error::new)?;
3900 Ok(Some((contract, client)))
3901 } else {
3902 Ok(None)
3903 }
3904 }
3905
3906 async fn pay(
3907 &self,
3908 invoice: Bolt11Invoice,
3909 max_delay: u64,
3910 max_fee: Amount,
3911 ) -> std::result::Result<[u8; 32], LightningRpcError> {
3912 let lightning_context = self.await_lightning_context().await;
3915 lightning_context
3916 .lnrpc
3917 .pay(invoice, max_delay, max_fee)
3918 .await
3919 .map(|response| response.preimage.0)
3920 }
3921
3922 async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
3923 self.await_outbound_payment_exists(payment_hash).await
3924 }
3925
3926 async fn min_contract_amount(
3927 &self,
3928 federation_id: &FederationId,
3929 amount: u64,
3930 ) -> std::result::Result<Amount, GatewayClientV2Error> {
3931 Ok(self
3932 .routing_info_v2(federation_id)
3933 .await
3934 .map_err(GatewayClientV2Error::new)?
3935 .ok_or_else(|| GatewayClientV2Error::new("Routing Info not available"))?
3936 .send_fee_minimum
3937 .add_to(amount))
3938 }
3939
3940 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
3941 let rhints = invoice.route_hints();
3942 let hop = rhints.first().and_then(|rh| rh.0.last())?;
3943
3944 let lightning_context = self.await_lightning_context().await;
3947 if hop.src_node_id != lightning_context.lightning_public_key {
3948 return None;
3949 }
3950
3951 self.federation_manager
3952 .read()
3953 .await
3954 .get_client_for_index(hop.short_channel_id)
3955 }
3956
3957 async fn relay_lnv1_swap(
3958 &self,
3959 client: &ClientHandleArc,
3960 invoice: &Bolt11Invoice,
3961 allow_fresh_dispatch: bool,
3962 ) -> std::result::Result<Option<FinalReceiveState>, GatewayClientV2Error> {
3963 let swap_params =
3964 SwapParameters {
3965 payment_hash: *invoice.payment_hash(),
3966 amount_msat: Amount::from_msats(invoice.amount_milli_satoshis().ok_or_else(
3967 || GatewayClientV2Error::new("Amountless invoice not supported"),
3968 )?),
3969 };
3970 let lnv1 = client
3971 .get_first_module::<GatewayClientModule>()
3972 .expect("No LNv1 module");
3973 let Some(operation_id) = lnv1
3974 .gateway_handle_direct_swap(swap_params, allow_fresh_dispatch)
3975 .await
3976 .map_err(GatewayClientV2Error::new)?
3977 else {
3978 return Ok(None);
3979 };
3980 let mut stream = lnv1
3981 .gateway_subscribe_ln_receive(operation_id)
3982 .await
3983 .map_err(GatewayClientV2Error::new)?
3984 .into_stream();
3985 let mut final_state = FinalReceiveState::Failure;
3986 while let Some(update) = stream.next().await {
3987 match update {
3988 GatewayExtReceiveStates::Funding => {}
3989 GatewayExtReceiveStates::FundingFailed { error: _ } => {
3990 final_state = FinalReceiveState::Rejected;
3991 }
3992 GatewayExtReceiveStates::Preimage(preimage) => {
3993 final_state = FinalReceiveState::Success(preimage.0);
3994 }
3995 GatewayExtReceiveStates::RefundError {
3996 error_message: _,
3997 error: _,
3998 } => {
3999 final_state = FinalReceiveState::Failure;
4000 }
4001 GatewayExtReceiveStates::RefundSuccess {
4002 out_points: _,
4003 error: _,
4004 } => {
4005 final_state = FinalReceiveState::Refunded;
4006 }
4007 }
4008 }
4009
4010 Ok(Some(final_state))
4011 }
4012
4013 async fn claim_payment_image(
4014 &self,
4015 payment_image: &PaymentImage,
4016 operation_id: OperationId,
4017 ) -> bool {
4018 self.gateway_db
4022 .autocommit(
4023 |dbtx, _| {
4024 let payment_image = payment_image.clone();
4025 Box::pin(async move {
4026 let claimer = dbtx
4027 .claim_outgoing_payment_image(payment_image, operation_id)
4028 .await;
4029 Ok::<_, std::convert::Infallible>(claimer == operation_id)
4030 })
4031 },
4032 None,
4033 )
4034 .await
4035 .expect("Retries until the transaction commits")
4036 }
4037}
4038
4039#[async_trait]
4040impl IGatewayClientV1 for Gateway {
4041 async fn verify_preimage_authentication(
4042 &self,
4043 payment_hash: sha256::Hash,
4044 preimage_auth: sha256::Hash,
4045 contract: OutgoingContractAccount,
4046 ) -> std::result::Result<(), OutgoingPaymentError> {
4047 let mut dbtx = self.gateway_db.begin_transaction().await;
4048 if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
4049 if !PreimageAuth::new(secret_hash).verifies(preimage_auth) {
4050 return Err(OutgoingPaymentError {
4051 error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
4052 contract_id: contract.contract.contract_id(),
4053 contract: Some(contract),
4054 });
4055 }
4056 } else {
4057 dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
4060 .await;
4061 return dbtx
4062 .commit_tx_result()
4063 .await
4064 .map_err(|_| OutgoingPaymentError {
4065 error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
4066 contract_id: contract.contract.contract_id(),
4067 contract: Some(contract),
4068 });
4069 }
4070
4071 Ok(())
4072 }
4073
4074 async fn verify_pruned_invoice(
4075 &self,
4076 payment_data: PaymentData,
4077 ) -> std::result::Result<(), GatewayClientV1Error> {
4078 if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
4079 let lightning_context = self
4080 .get_lightning_context()
4081 .await
4082 .map_err(GatewayClientV1Error::new)?;
4083
4084 if !lightning_context.lnrpc.supports_private_payments() {
4085 return Err(GatewayClientV1Error::new(
4086 "Private payments are not supported by the lightning node",
4087 ));
4088 }
4089 }
4090
4091 Ok(())
4092 }
4093
4094 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
4095 let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
4096 let lightning_fee = gateway_dbtx
4097 .load_federation_config(federation_id)
4098 .await?
4099 .lightning_fee;
4100
4101 RoutingFees::try_from(lightning_fee)
4105 .inspect_err(|err| {
4106 warn!(
4107 target: LOG_GATEWAY,
4108 %federation_id,
4109 err = %err.fmt_compact(),
4110 "Configured lightning fee cannot be used. Set a smaller fee with `set_fees`."
4111 );
4112 })
4113 .ok()
4114 }
4115
4116 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
4117 self.federation_manager
4118 .read()
4119 .await
4120 .client(federation_id)
4121 .cloned()
4122 }
4123
4124 async fn get_client_for_invoice(
4125 &self,
4126 payment_data: PaymentData,
4127 ) -> Option<Spanned<ClientHandleArc>> {
4128 let rhints = payment_data.route_hints();
4129 let hop = rhints.first().and_then(|rh| rh.0.last())?;
4130
4131 let lightning_context = self.await_lightning_context().await;
4134 if hop.src_node_id != lightning_context.lightning_public_key {
4135 return None;
4136 }
4137
4138 self.federation_manager
4139 .read()
4140 .await
4141 .get_client_for_index(hop.short_channel_id)
4142 }
4143
4144 async fn pay(
4145 &self,
4146 payment_data: PaymentData,
4147 max_delay: u64,
4148 max_fee: Amount,
4149 ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
4150 let lightning_context = self.await_lightning_context().await;
4153
4154 match payment_data {
4155 PaymentData::Invoice(invoice) => {
4156 lightning_context
4157 .lnrpc
4158 .pay(invoice, max_delay, max_fee)
4159 .await
4160 }
4161 PaymentData::PrunedInvoice(invoice) => {
4162 lightning_context
4163 .lnrpc
4164 .pay_private(invoice, max_delay, max_fee)
4165 .await
4166 }
4167 }
4168 }
4169
4170 async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
4171 self.await_outbound_payment_exists(payment_hash).await
4172 }
4173
4174 async fn complete_htlc(
4175 &self,
4176 htlc: InterceptPaymentResponse,
4177 ) -> std::result::Result<(), LightningRpcError> {
4178 self.await_complete_htlc(htlc).await
4179 }
4180
4181 async fn is_lnv2_direct_swap(
4182 &self,
4183 payment_hash: sha256::Hash,
4184 amount: Amount,
4185 ) -> std::result::Result<
4186 Option<(
4187 fedimint_lnv2_common::contracts::IncomingContract,
4188 ClientHandleArc,
4189 )>,
4190 GatewayClientV1Error,
4191 > {
4192 let (contract, client) = self
4193 .get_registered_incoming_contract_and_client_v2(
4194 PaymentImage::Hash(payment_hash),
4195 amount.msats,
4196 )
4197 .await
4198 .map_err(GatewayClientV1Error::new)?;
4199 Ok(Some((contract, client)))
4200 }
4201}