1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_possible_wrap)]
4#![allow(clippy::cast_sign_loss)]
5#![allow(clippy::default_trait_access)]
6#![allow(clippy::doc_markdown)]
7#![allow(clippy::missing_errors_doc)]
8#![allow(clippy::missing_panics_doc)]
9#![allow(clippy::module_name_repetitions)]
10#![allow(clippy::must_use_candidate)]
11#![allow(clippy::return_self_not_must_use)]
12#![allow(clippy::similar_names)]
13#![allow(clippy::too_many_lines)]
14#![allow(clippy::large_futures)]
15#![allow(clippy::struct_field_names)]
16
17pub mod client;
18pub mod config;
19pub mod envs;
20mod error;
21mod events;
22mod federation_manager;
23mod iroh_server;
24mod metrics;
25pub mod rpc_server;
26mod types;
27
28use std::collections::{BTreeMap, BTreeSet};
29use std::env;
30use std::fmt::Display;
31use std::net::SocketAddr;
32use std::str::FromStr;
33use std::sync::Arc;
34use std::time::{Duration, UNIX_EPOCH};
35
36use anyhow::{Context, anyhow, ensure};
37use async_trait::async_trait;
38use bitcoin::hashes::sha256;
39use bitcoin::{Address, Network, Txid, secp256k1};
40use clap::Parser;
41use client::GatewayClientBuilder;
42pub use config::GatewayParameters;
43use config::{DatabaseBackend, GatewayOpts};
44use envs::FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV;
45use error::FederationNotConnected;
46use events::ALL_GATEWAY_EVENTS;
47use federation_manager::FederationManager;
48use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic};
49use fedimint_bitcoind::bitcoincore::BitcoindClient;
50use fedimint_bitcoind::{EsploraClient, IBitcoindRpc};
51use fedimint_client::module_init::ClientModuleInitRegistry;
52use fedimint_client::secret::RootSecretStrategy;
53use fedimint_client::{Client, ClientHandleArc};
54use fedimint_core::config::FederationId;
55use fedimint_core::core::OperationId;
56use fedimint_core::db::{Committable, Database, DatabaseTransaction, apply_migrations};
57use fedimint_core::envs::is_env_var_set;
58use fedimint_core::invite_code::InviteCode;
59use fedimint_core::module::CommonModuleInit;
60use fedimint_core::module::registry::ModuleDecoderRegistry;
61use fedimint_core::rustls::install_crypto_provider;
62use fedimint_core::secp256k1::PublicKey;
63use fedimint_core::secp256k1::schnorr::Signature;
64use fedimint_core::task::{TaskGroup, TaskHandle, TaskShutdownToken, sleep};
65use fedimint_core::time::duration_since_epoch;
66use fedimint_core::util::backoff_util::fibonacci_max_one_hour;
67use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl, Spanned, retry};
68use fedimint_core::{
69 Amount, BitcoinAmountOrAll, crit, fedimint_build_code_version_env, get_network_for_address,
70};
71use fedimint_eventlog::{DBTransactionEventLogExt, EventLogId, StructuredPaymentEvents};
72use fedimint_gateway_common::{
73 BackupPayload, ChainSource, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse,
74 ConnectFedPayload, ConnectorType, CreateInvoiceForOperatorPayload, CreateOfferPayload,
75 CreateOfferResponse, DepositAddressPayload, DepositAddressRecheckPayload,
76 FederationBalanceInfo, FederationConfig, FederationInfo, GatewayBalances, GatewayFedConfig,
77 GatewayInfo, GetInvoiceRequest, GetInvoiceResponse, LeaveFedPayload, LightningInfo,
78 LightningMode, ListTransactionsPayload, ListTransactionsResponse, MnemonicResponse,
79 OpenChannelRequest, PayInvoiceForOperatorPayload, PayOfferPayload, PayOfferResponse,
80 PaymentLogPayload, PaymentLogResponse, PaymentStats, PaymentSummaryPayload,
81 PaymentSummaryResponse, ReceiveEcashPayload, ReceiveEcashResponse, RegisteredProtocol,
82 SendOnchainRequest, SetFeesPayload, SetMnemonicPayload, SpendEcashPayload, SpendEcashResponse,
83 V1_API_ENDPOINT, WithdrawPayload, WithdrawPreviewPayload, WithdrawPreviewResponse,
84 WithdrawResponse,
85};
86use fedimint_gateway_server_db::{GatewayDbtxNcExt as _, get_gatewayd_database_migrations};
87pub use fedimint_gateway_ui::IAdminGateway;
88use fedimint_gw_client::events::compute_lnv1_stats;
89use fedimint_gw_client::pay::{OutgoingPaymentError, OutgoingPaymentErrorType};
90use fedimint_gw_client::{
91 GatewayClientModule, GatewayExtPayStates, GatewayExtReceiveStates, IGatewayClientV1,
92 SwapParameters,
93};
94use fedimint_gwv2_client::events::compute_lnv2_stats;
95use fedimint_gwv2_client::{
96 EXPIRATION_DELTA_MINIMUM_V2, FinalReceiveState, GatewayClientModuleV2, IGatewayClientV2,
97};
98use fedimint_lightning::lnd::GatewayLndClient;
99use fedimint_lightning::{
100 CreateInvoiceRequest, ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse,
101 InvoiceDescription, LightningContext, LightningRpcError, LnRpcTracked, PayInvoiceResponse,
102 PaymentAction, RouteHtlcStream, ldk,
103};
104use fedimint_ln_client::pay::PaymentData;
105use fedimint_ln_common::LightningCommonInit;
106use fedimint_ln_common::config::LightningClientConfig;
107use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
108use fedimint_ln_common::contracts::{IdentifiableContract, Preimage};
109use fedimint_lnurl::VerifyResponse;
110use fedimint_lnv2_common::Bolt11InvoiceDescription;
111use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
112use fedimint_lnv2_common::gateway_api::{
113 CreateBolt11InvoicePayload, PaymentFee, RoutingInfo, SendPaymentPayload,
114};
115use fedimint_logging::LOG_GATEWAY;
116use fedimint_mint_client::{
117 MintClientInit, MintClientModule, SelectNotesWithAtleastAmount, SelectNotesWithExactAmount,
118};
119use fedimint_wallet_client::{PegOutFees, WalletClientInit, WalletClientModule, WithdrawState};
120use futures::stream::StreamExt;
121use lightning_invoice::{Bolt11Invoice, RoutingFees};
122use rand::rngs::OsRng;
123use tokio::sync::RwLock;
124use tracing::{debug, info, info_span, warn};
125
126use crate::envs::FM_GATEWAY_MNEMONIC_ENV;
127use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError};
128use crate::events::get_events_for_duration;
129use crate::rpc_server::run_webserver;
130use crate::types::PrettyInterceptPaymentRequest;
131
132const GW_ANNOUNCEMENT_TTL: Duration = Duration::from_secs(600);
134
135const DEFAULT_NUM_ROUTE_HINTS: u32 = 1;
138
139pub const DEFAULT_NETWORK: Network = Network::Regtest;
141
142pub type Result<T> = std::result::Result<T, PublicGatewayError>;
143pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
144
145const DB_FILE: &str = "gatewayd.db";
148
149const LDK_NODE_DB_FOLDER: &str = "ldk_node";
152
153#[cfg_attr(doc, aquamarine::aquamarine)]
154#[derive(Clone, Debug)]
167pub enum GatewayState {
168 NotConfigured {
169 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
172 },
173 Disconnected,
174 Syncing,
175 Connected,
176 Running {
177 lightning_context: LightningContext,
178 },
179 ShuttingDown {
180 lightning_context: LightningContext,
181 },
182}
183
184impl Display for GatewayState {
185 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
186 match self {
187 GatewayState::NotConfigured { .. } => write!(f, "NotConfigured"),
188 GatewayState::Disconnected => write!(f, "Disconnected"),
189 GatewayState::Syncing => write!(f, "Syncing"),
190 GatewayState::Connected => write!(f, "Connected"),
191 GatewayState::Running { .. } => write!(f, "Running"),
192 GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"),
193 }
194 }
195}
196
197#[derive(Debug, Clone)]
200struct Registration {
201 endpoint_url: SafeUrl,
203
204 keypair: secp256k1::Keypair,
206}
207
208impl Registration {
209 pub async fn new(db: &Database, endpoint_url: SafeUrl, protocol: RegisteredProtocol) -> Self {
210 let keypair = Gateway::load_or_create_gateway_keypair(db, protocol).await;
211 Self {
212 endpoint_url,
213 keypair,
214 }
215 }
216}
217
218enum ReceivePaymentStreamAction {
220 RetryAfterDelay,
221 NoRetry,
222}
223
224#[derive(Clone)]
225pub struct Gateway {
226 federation_manager: Arc<RwLock<FederationManager>>,
228
229 lightning_mode: LightningMode,
231
232 state: Arc<RwLock<GatewayState>>,
234
235 client_builder: GatewayClientBuilder,
238
239 gateway_db: Database,
241
242 listen: SocketAddr,
244
245 metrics_listen: SocketAddr,
247
248 task_group: TaskGroup,
250
251 bcrypt_password_hash: Arc<bcrypt::HashParts>,
254
255 num_route_hints: u32,
257
258 network: Network,
260
261 chain_source: ChainSource,
263
264 default_routing_fees: PaymentFee,
266
267 default_transaction_fees: PaymentFee,
269
270 iroh_sk: iroh::SecretKey,
272
273 iroh_listen: Option<SocketAddr>,
275
276 iroh_dns: Option<SafeUrl>,
278
279 iroh_relays: Vec<SafeUrl>,
282
283 registrations: BTreeMap<RegisteredProtocol, Registration>,
286}
287
288impl std::fmt::Debug for Gateway {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 f.debug_struct("Gateway")
291 .field("federation_manager", &self.federation_manager)
292 .field("state", &self.state)
293 .field("client_builder", &self.client_builder)
294 .field("gateway_db", &self.gateway_db)
295 .field("listen", &self.listen)
296 .field("registrations", &self.registrations)
297 .finish_non_exhaustive()
298 }
299}
300
301struct WithdrawDetails {
303 amount: Amount,
304 mint_fees: Option<Amount>,
305 peg_out_fees: PegOutFees,
306}
307
308async fn calculate_max_withdrawable(
310 client: &ClientHandleArc,
311 address: &Address,
312) -> AdminResult<WithdrawDetails> {
313 let wallet_module = client.get_first_module::<WalletClientModule>()?;
314
315 let balance = client.get_balance_for_btc().await.map_err(|err| {
316 AdminGatewayError::Unexpected(anyhow!(
317 "Balance not available: {}",
318 err.fmt_compact_anyhow()
319 ))
320 })?;
321
322 let peg_out_fees = wallet_module
323 .get_withdraw_fees(
324 address,
325 bitcoin::Amount::from_sat(balance.sats_round_down()),
326 )
327 .await?;
328
329 let max_withdrawable_before_mint_fees = balance
330 .checked_sub(peg_out_fees.amount().into())
331 .ok_or_else(|| AdminGatewayError::WithdrawError {
332 failure_reason: "Insufficient balance to cover peg-out fees".to_string(),
333 })?;
334
335 let mint_module = client.get_first_module::<MintClientModule>()?;
336 let mint_fees = mint_module.estimate_spend_all_fees().await;
337
338 let max_withdrawable = max_withdrawable_before_mint_fees.saturating_sub(mint_fees);
339
340 Ok(WithdrawDetails {
341 amount: max_withdrawable,
342 mint_fees: Some(mint_fees),
343 peg_out_fees,
344 })
345}
346
347impl Gateway {
348 #[allow(clippy::too_many_arguments)]
351 pub async fn new_with_custom_registry(
352 lightning_mode: LightningMode,
353 client_builder: GatewayClientBuilder,
354 listen: SocketAddr,
355 api_addr: SafeUrl,
356 bcrypt_password_hash: bcrypt::HashParts,
357 network: Network,
358 num_route_hints: u32,
359 gateway_db: Database,
360 gateway_state: GatewayState,
361 chain_source: ChainSource,
362 iroh_listen: Option<SocketAddr>,
363 ) -> anyhow::Result<Gateway> {
364 let versioned_api = api_addr
365 .join(V1_API_ENDPOINT)
366 .expect("Failed to version gateway API address");
367 let metrics_listen = SocketAddr::new(
369 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
370 listen.port() + 1,
371 );
372 Gateway::new(
373 lightning_mode,
374 GatewayParameters {
375 listen,
376 versioned_api: Some(versioned_api),
377 bcrypt_password_hash,
378 network,
379 num_route_hints,
380 default_routing_fees: PaymentFee::TRANSACTION_FEE_DEFAULT,
381 default_transaction_fees: PaymentFee::TRANSACTION_FEE_DEFAULT,
382 iroh_listen,
383 iroh_dns: None,
384 iroh_relays: vec![],
385 skip_setup: true,
386 metrics_listen,
387 },
388 gateway_db,
389 client_builder,
390 gateway_state,
391 chain_source,
392 )
393 .await
394 }
395
396 fn get_bitcoind_client(
399 opts: &GatewayOpts,
400 network: bitcoin::Network,
401 gateway_id: &PublicKey,
402 ) -> anyhow::Result<(BitcoindClient, ChainSource)> {
403 let bitcoind_username = opts
404 .bitcoind_username
405 .clone()
406 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
407 let url = opts.bitcoind_url.clone().expect("No bitcoind url set");
408 let password = opts
409 .bitcoind_password
410 .clone()
411 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
412
413 let chain_source = ChainSource::Bitcoind {
414 username: bitcoind_username.clone(),
415 password: password.clone(),
416 server_url: url.clone(),
417 };
418 let wallet_name = format!("gatewayd-{gateway_id}");
419 let client = BitcoindClient::new(&url, bitcoind_username, password, &wallet_name, network)?;
420 Ok((client, chain_source))
421 }
422
423 pub async fn new_with_default_modules(
426 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
427 ) -> anyhow::Result<Gateway> {
428 let opts = GatewayOpts::parse();
429 let gateway_parameters = opts.to_gateway_parameters()?;
430 let decoders = ModuleDecoderRegistry::default();
431
432 let db_path = opts.data_dir.join(DB_FILE);
433 let gateway_db = match opts.db_backend {
434 DatabaseBackend::RocksDb => {
435 debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
436 Database::new(
437 fedimint_rocksdb::RocksDb::build(db_path).open().await?,
438 decoders,
439 )
440 }
441 DatabaseBackend::CursedRedb => {
442 debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
443 Database::new(
444 fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
445 decoders,
446 )
447 }
448 };
449
450 apply_migrations(
453 &gateway_db,
454 (),
455 "gatewayd".to_string(),
456 get_gatewayd_database_migrations(),
457 None,
458 None,
459 )
460 .await?;
461
462 let http_id = Self::load_or_create_gateway_keypair(&gateway_db, RegisteredProtocol::Http)
465 .await
466 .public_key();
467 let (dyn_bitcoin_rpc, chain_source) =
468 match (opts.bitcoind_url.as_ref(), opts.esplora_url.as_ref()) {
469 (Some(_), None) => {
470 let (client, chain_source) =
471 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
472 (client.into_dyn(), chain_source)
473 }
474 (None, Some(url)) => {
475 let client = EsploraClient::new(url)
476 .expect("Could not create EsploraClient")
477 .into_dyn();
478 let chain_source = ChainSource::Esplora {
479 server_url: url.clone(),
480 };
481 (client, chain_source)
482 }
483 (Some(_), Some(_)) => {
484 let (client, chain_source) =
486 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
487 (client.into_dyn(), chain_source)
488 }
489 _ => unreachable!("ArgGroup already enforced XOR relation"),
490 };
491
492 let mut registry = ClientModuleInitRegistry::new();
495 registry.attach(MintClientInit);
496 registry.attach(WalletClientInit::new(dyn_bitcoin_rpc));
497
498 let client_builder =
499 GatewayClientBuilder::new(opts.data_dir.clone(), registry, opts.db_backend).await?;
500
501 let gateway_state = if Self::load_mnemonic(&gateway_db).await.is_some() {
502 GatewayState::Disconnected
503 } else {
504 if gateway_parameters.skip_setup {
507 let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
508 info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
509 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
510 |e| {
511 AdminGatewayError::MnemonicError(anyhow!(format!(
512 "Seed phrase provided in environment was invalid {e:?}"
513 )))
514 },
515 )?
516 } else {
517 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
518 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
519 };
520
521 Client::store_encodable_client_secret(&gateway_db, mnemonic.to_entropy())
522 .await
523 .map_err(AdminGatewayError::MnemonicError)?;
524 GatewayState::Disconnected
525 } else {
526 GatewayState::NotConfigured { mnemonic_sender }
527 }
528 };
529
530 info!(
531 target: LOG_GATEWAY,
532 version = %fedimint_build_code_version_env!(),
533 "Starting gatewayd",
534 );
535
536 Gateway::new(
537 opts.mode,
538 gateway_parameters,
539 gateway_db,
540 client_builder,
541 gateway_state,
542 chain_source,
543 )
544 .await
545 }
546
547 async fn new(
550 lightning_mode: LightningMode,
551 gateway_parameters: GatewayParameters,
552 gateway_db: Database,
553 client_builder: GatewayClientBuilder,
554 gateway_state: GatewayState,
555 chain_source: ChainSource,
556 ) -> anyhow::Result<Gateway> {
557 let num_route_hints = gateway_parameters.num_route_hints;
558 let network = gateway_parameters.network;
559
560 let task_group = TaskGroup::new();
561 task_group.install_kill_handler();
562
563 let mut registrations = BTreeMap::new();
564 if let Some(http_url) = gateway_parameters.versioned_api {
565 registrations.insert(
566 RegisteredProtocol::Http,
567 Registration::new(&gateway_db, http_url, RegisteredProtocol::Http).await,
568 );
569 }
570
571 let iroh_sk = Self::load_or_create_iroh_key(&gateway_db).await;
572 if gateway_parameters.iroh_listen.is_some() {
573 let endpoint_url = SafeUrl::parse(&format!("iroh://{}", iroh_sk.public()))?;
574 registrations.insert(
575 RegisteredProtocol::Iroh,
576 Registration::new(&gateway_db, endpoint_url, RegisteredProtocol::Iroh).await,
577 );
578 }
579
580 Ok(Self {
581 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
582 lightning_mode,
583 state: Arc::new(RwLock::new(gateway_state)),
584 client_builder,
585 gateway_db: gateway_db.clone(),
586 listen: gateway_parameters.listen,
587 metrics_listen: gateway_parameters.metrics_listen,
588 task_group,
589 bcrypt_password_hash: Arc::new(gateway_parameters.bcrypt_password_hash),
590 num_route_hints,
591 network,
592 chain_source,
593 default_routing_fees: gateway_parameters.default_routing_fees,
594 default_transaction_fees: gateway_parameters.default_transaction_fees,
595 iroh_sk,
596 iroh_dns: gateway_parameters.iroh_dns,
597 iroh_relays: gateway_parameters.iroh_relays,
598 iroh_listen: gateway_parameters.iroh_listen,
599 registrations,
600 })
601 }
602
603 async fn load_or_create_gateway_keypair(
604 gateway_db: &Database,
605 protocol: RegisteredProtocol,
606 ) -> secp256k1::Keypair {
607 let mut dbtx = gateway_db.begin_transaction().await;
608 let keypair = dbtx.load_or_create_gateway_keypair(protocol).await;
609 dbtx.commit_tx().await;
610 keypair
611 }
612
613 async fn load_or_create_iroh_key(gateway_db: &Database) -> iroh::SecretKey {
616 let mut dbtx = gateway_db.begin_transaction().await;
617 let iroh_sk = dbtx.load_or_create_iroh_key().await;
618 dbtx.commit_tx().await;
619 iroh_sk
620 }
621
622 pub async fn http_gateway_id(&self) -> PublicKey {
623 Self::load_or_create_gateway_keypair(&self.gateway_db, RegisteredProtocol::Http)
624 .await
625 .public_key()
626 }
627
628 async fn get_state(&self) -> GatewayState {
629 self.state.read().await.clone()
630 }
631
632 pub async fn dump_database(
635 dbtx: &mut DatabaseTransaction<'_>,
636 prefix_names: Vec<String>,
637 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
638 dbtx.dump_database(prefix_names).await
639 }
640
641 pub async fn run(
646 self,
647 runtime: Arc<tokio::runtime::Runtime>,
648 mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
649 ) -> anyhow::Result<TaskShutdownToken> {
650 install_crypto_provider().await;
651 self.register_clients_timer();
652 self.load_clients().await?;
653 self.start_gateway(runtime, mnemonic_receiver.resubscribe());
654 self.spawn_backup_task();
655 fedimint_metrics::spawn_api_server(self.metrics_listen, self.task_group.clone()).await?;
657 let handle = self.task_group.make_handle();
659 run_webserver(Arc::new(self), mnemonic_receiver.resubscribe()).await?;
660 let shutdown_receiver = handle.make_shutdown_rx();
661 Ok(shutdown_receiver)
662 }
663
664 fn spawn_backup_task(&self) {
667 let self_copy = self.clone();
668 self.task_group
669 .spawn_cancellable_silent("backup ecash", async move {
670 const BACKUP_UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 60);
671 let mut interval = tokio::time::interval(BACKUP_UPDATE_INTERVAL);
672 interval.tick().await;
673 loop {
674 {
675 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
676 self_copy.backup_all_federations(&mut dbtx).await;
677 dbtx.commit_tx().await;
678 interval.tick().await;
679 }
680 }
681 });
682 }
683
684 pub async fn backup_all_federations(&self, dbtx: &mut DatabaseTransaction<'_, Committable>) {
688 const BACKUP_THRESHOLD_DURATION: Duration = Duration::from_secs(24 * 60 * 60);
691
692 let now = fedimint_core::time::now();
693 let threshold = now
694 .checked_sub(BACKUP_THRESHOLD_DURATION)
695 .expect("Cannot be negative");
696 for (id, last_backup) in dbtx.load_backup_records().await {
697 match last_backup {
698 Some(backup_time) if backup_time < threshold => {
699 let fed_manager = self.federation_manager.read().await;
700 fed_manager.backup_federation(&id, dbtx, now).await;
701 }
702 None => {
703 let fed_manager = self.federation_manager.read().await;
704 fed_manager.backup_federation(&id, dbtx, now).await;
705 }
706 _ => {}
707 }
708 }
709 }
710
711 fn start_gateway(
714 &self,
715 runtime: Arc<tokio::runtime::Runtime>,
716 mut mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
717 ) {
718 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 60;
719
720 let self_copy = self.clone();
721 let tg = self.task_group.clone();
722 self.task_group.spawn(
723 "Subscribe to intercepted lightning payments in stream",
724 |handle| async move {
725 loop {
727 if handle.is_shutting_down() {
728 info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
729 break;
730 }
731
732 if let GatewayState::NotConfigured{ .. } = self_copy.get_state().await {
733 info!(
734 target: LOG_GATEWAY,
735 "Waiting for the mnemonic to be set before starting lightning receive loop."
736 );
737 info!(
738 target: LOG_GATEWAY,
739 "You might need to provide it from the UI or refer to documentation w.r.t how to initialize it."
740 );
741
742 let _ = mnemonic_receiver.recv().await;
743 info!(
744 target: LOG_GATEWAY,
745 "Received mnemonic, attempting to start lightning receive loop"
746 );
747 }
748
749 let payment_stream_task_group = tg.make_subgroup();
750 let lnrpc_route = self_copy.create_lightning_client(runtime.clone()).await;
751
752 debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
753 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
754 {
755 Ok((stream, ln_client)) => (stream, ln_client),
756 Err(err) => {
757 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
758 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
759 continue
760 }
761 };
762
763 self_copy.set_gateway_state(GatewayState::Connected).await;
765 info!(target: LOG_GATEWAY, "Established lightning payment stream");
766
767 let route_payments_response =
768 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
769
770 self_copy.set_gateway_state(GatewayState::Disconnected).await;
771 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
772 crit!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Lightning payment stream task group shutdown");
773 }
774
775 self_copy.unannounce_from_all_federations().await;
776
777 match route_payments_response {
778 ReceivePaymentStreamAction::RetryAfterDelay => {
779 warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
780 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
781 }
782 ReceivePaymentStreamAction::NoRetry => break,
783 }
784 }
785 },
786 );
787 }
788
789 async fn route_lightning_payments<'a>(
793 &'a self,
794 handle: &TaskHandle,
795 mut stream: RouteHtlcStream<'a>,
796 ln_client: Arc<dyn ILnRpcClient>,
797 ) -> ReceivePaymentStreamAction {
798 let LightningInfo::Connected {
799 public_key: lightning_public_key,
800 alias: lightning_alias,
801 network: lightning_network,
802 block_height: _,
803 synced_to_chain,
804 } = ln_client.parsed_node_info().await
805 else {
806 warn!(target: LOG_GATEWAY, "Failed to retrieve Lightning info");
807 return ReceivePaymentStreamAction::RetryAfterDelay;
808 };
809
810 assert!(
811 self.network == lightning_network,
812 "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
813 self.network
814 );
815
816 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
817 info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
818 } else {
819 self.set_gateway_state(GatewayState::Syncing).await;
820 info!(target: LOG_GATEWAY, "Waiting for chain sync");
821 if let Err(err) = ln_client.wait_for_chain_sync().await {
822 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
823 return ReceivePaymentStreamAction::RetryAfterDelay;
824 }
825 }
826
827 let lightning_context = LightningContext {
828 lnrpc: LnRpcTracked::new(ln_client, "gateway"),
829 lightning_public_key,
830 lightning_alias,
831 lightning_network,
832 };
833 self.set_gateway_state(GatewayState::Running { lightning_context })
834 .await;
835 info!(target: LOG_GATEWAY, "Gateway is running");
836
837 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
838 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
841 let all_federations_configs =
842 dbtx.load_federation_configs().await.into_iter().collect();
843 self.register_federations(&all_federations_configs, &self.task_group)
844 .await;
845 }
846
847 let htlc_task_group = self.task_group.make_subgroup();
850 if handle
851 .cancel_on_shutdown(async move {
852 loop {
853 let payment_request_or = tokio::select! {
854 payment_request_or = stream.next() => {
855 payment_request_or
856 }
857 () = self.is_shutting_down_safely() => {
858 break;
859 }
860 };
861
862 let Some(payment_request) = payment_request_or else {
863 warn!(
864 target: LOG_GATEWAY,
865 "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
866 );
867 break;
868 };
869
870 let state_guard = self.state.read().await;
871 if let GatewayState::Running { ref lightning_context } = *state_guard {
872 let gateway = self.clone();
874 let lightning_context = lightning_context.clone();
875 htlc_task_group.spawn_cancellable_silent(
876 "handle_lightning_payment",
877 async move {
878 let start = fedimint_core::time::now();
879 let outcome = gateway
880 .handle_lightning_payment(payment_request, &lightning_context)
881 .await;
882 metrics::HTLC_HANDLING_DURATION_SECONDS
883 .with_label_values(&[outcome])
884 .observe(
885 fedimint_core::time::now()
886 .duration_since(start)
887 .unwrap_or_default()
888 .as_secs_f64(),
889 );
890 },
891 );
892 } else {
893 warn!(
894 target: LOG_GATEWAY,
895 state = %state_guard,
896 "Gateway isn't in a running state, cannot handle incoming payments."
897 );
898 break;
899 }
900 }
901 })
902 .await
903 .is_ok()
904 {
905 warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
906 ReceivePaymentStreamAction::RetryAfterDelay
907 } else {
908 info!(target: LOG_GATEWAY, "Received shutdown signal");
909 ReceivePaymentStreamAction::NoRetry
910 }
911 }
912
913 async fn is_shutting_down_safely(&self) {
916 loop {
917 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
918 return;
919 }
920
921 fedimint_core::task::sleep(Duration::from_secs(1)).await;
922 }
923 }
924
925 async fn handle_lightning_payment(
932 &self,
933 payment_request: InterceptPaymentRequest,
934 lightning_context: &LightningContext,
935 ) -> &'static str {
936 info!(
937 target: LOG_GATEWAY,
938 lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
939 "Intercepting lightning payment",
940 );
941
942 let lnv2_start = fedimint_core::time::now();
943 let lnv2_result = self
944 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
945 .await;
946 let lnv2_outcome = if lnv2_result.is_ok() {
947 "success"
948 } else {
949 "error"
950 };
951 metrics::HTLC_LNV2_ATTEMPT_DURATION_SECONDS
952 .with_label_values(&[lnv2_outcome])
953 .observe(
954 fedimint_core::time::now()
955 .duration_since(lnv2_start)
956 .unwrap_or_default()
957 .as_secs_f64(),
958 );
959 if lnv2_result.is_ok() {
960 return "lnv2";
961 }
962
963 let lnv1_start = fedimint_core::time::now();
964 let lnv1_result = self
965 .try_handle_lightning_payment_ln_legacy(&payment_request)
966 .await;
967 let lnv1_outcome = if lnv1_result.is_ok() {
968 "success"
969 } else {
970 "error"
971 };
972 metrics::HTLC_LNV1_ATTEMPT_DURATION_SECONDS
973 .with_label_values(&[lnv1_outcome])
974 .observe(
975 fedimint_core::time::now()
976 .duration_since(lnv1_start)
977 .unwrap_or_default()
978 .as_secs_f64(),
979 );
980 if lnv1_result.is_ok() {
981 return "lnv1";
982 }
983
984 Self::forward_lightning_payment(payment_request, lightning_context).await;
985 "forward"
986 }
987
988 async fn try_handle_lightning_payment_lnv2(
991 &self,
992 htlc_request: &InterceptPaymentRequest,
993 lightning_context: &LightningContext,
994 ) -> Result<()> {
995 let (contract, client) = self
1001 .get_registered_incoming_contract_and_client_v2(
1002 PaymentImage::Hash(htlc_request.payment_hash),
1003 htlc_request.amount_msat,
1004 )
1005 .await?;
1006
1007 if let Err(err) = client
1008 .get_first_module::<GatewayClientModuleV2>()
1009 .expect("Must have client module")
1010 .relay_incoming_htlc(
1011 htlc_request.payment_hash,
1012 htlc_request.incoming_chan_id,
1013 htlc_request.htlc_id,
1014 contract,
1015 htlc_request.amount_msat,
1016 )
1017 .await
1018 {
1019 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error relaying incoming lightning payment");
1020
1021 let outcome = InterceptPaymentResponse {
1022 action: PaymentAction::Cancel,
1023 payment_hash: htlc_request.payment_hash,
1024 incoming_chan_id: htlc_request.incoming_chan_id,
1025 htlc_id: htlc_request.htlc_id,
1026 };
1027
1028 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1029 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
1030 }
1031 }
1032
1033 Ok(())
1034 }
1035
1036 async fn try_handle_lightning_payment_ln_legacy(
1039 &self,
1040 htlc_request: &InterceptPaymentRequest,
1041 ) -> Result<()> {
1042 let Some(federation_index) = htlc_request.short_channel_id else {
1044 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1045 "Incoming payment has not last hop short channel id".to_string(),
1046 )));
1047 };
1048
1049 let Some(client) = self
1050 .federation_manager
1051 .read()
1052 .await
1053 .get_client_for_index(federation_index)
1054 else {
1055 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())));
1056 };
1057
1058 client
1059 .borrow()
1060 .with(|client| async {
1061 let htlc = htlc_request.clone().try_into();
1062 match htlc {
1063 Ok(htlc) => {
1064 let lnv1 =
1065 client
1066 .get_first_module::<GatewayClientModule>()
1067 .map_err(|_| {
1068 PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1069 "Federation does not have LNv1 module".to_string(),
1070 ))
1071 })?;
1072 match lnv1.gateway_handle_intercepted_htlc(htlc).await {
1073 Ok(_) => Ok(()),
1074 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1075 format!("Error intercepting lightning payment {e:?}"),
1076 ))),
1077 }
1078 }
1079 _ => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1080 "Could not convert InterceptHtlcResult into an HTLC".to_string(),
1081 ))),
1082 }
1083 })
1084 .await
1085 }
1086
1087 async fn forward_lightning_payment(
1091 htlc_request: InterceptPaymentRequest,
1092 lightning_context: &LightningContext,
1093 ) {
1094 let outcome = InterceptPaymentResponse {
1095 action: PaymentAction::Forward,
1096 payment_hash: htlc_request.payment_hash,
1097 incoming_chan_id: htlc_request.incoming_chan_id,
1098 htlc_id: htlc_request.htlc_id,
1099 };
1100
1101 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1102 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1103 }
1104 }
1105
1106 async fn set_gateway_state(&self, state: GatewayState) {
1108 let mut lock = self.state.write().await;
1109 *lock = state;
1110 }
1111
1112 pub async fn handle_get_federation_config(
1115 &self,
1116 federation_id_or: Option<FederationId>,
1117 ) -> AdminResult<GatewayFedConfig> {
1118 if !matches!(self.get_state().await, GatewayState::Running { .. }) {
1119 return Ok(GatewayFedConfig {
1120 federations: BTreeMap::new(),
1121 });
1122 }
1123
1124 let federations = if let Some(federation_id) = federation_id_or {
1125 let mut federations = BTreeMap::new();
1126 federations.insert(
1127 federation_id,
1128 self.federation_manager
1129 .read()
1130 .await
1131 .get_federation_config(federation_id)
1132 .await?,
1133 );
1134 federations
1135 } else {
1136 self.federation_manager
1137 .read()
1138 .await
1139 .get_all_federation_configs()
1140 .await
1141 };
1142
1143 Ok(GatewayFedConfig { federations })
1144 }
1145
1146 pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
1149 let (_, address, _) = self
1150 .select_client(payload.federation_id)
1151 .await?
1152 .value()
1153 .get_first_module::<WalletClientModule>()
1154 .expect("Must have client module")
1155 .allocate_deposit_address_expert_only(())
1156 .await?;
1157 Ok(address)
1158 }
1159
1160 async fn handle_pay_invoice_msg(
1163 &self,
1164 payload: fedimint_ln_client::pay::PayInvoicePayload,
1165 ) -> Result<Preimage> {
1166 let GatewayState::Running { .. } = self.get_state().await else {
1167 return Err(PublicGatewayError::Lightning(
1168 LightningRpcError::FailedToConnect,
1169 ));
1170 };
1171
1172 debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1173 let client = self.select_client(payload.federation_id).await?;
1174 let contract_id = payload.contract_id;
1175 let gateway_module = &client
1176 .value()
1177 .get_first_module::<GatewayClientModule>()
1178 .map_err(LNv1Error::OutgoingPayment)
1179 .map_err(PublicGatewayError::LNv1)?;
1180 let operation_id = gateway_module
1181 .gateway_pay_bolt11_invoice(payload)
1182 .await
1183 .map_err(LNv1Error::OutgoingPayment)
1184 .map_err(PublicGatewayError::LNv1)?;
1185 let mut updates = gateway_module
1186 .gateway_subscribe_ln_pay(operation_id)
1187 .await
1188 .map_err(LNv1Error::OutgoingPayment)
1189 .map_err(PublicGatewayError::LNv1)?
1190 .into_stream();
1191 while let Some(update) = updates.next().await {
1192 match update {
1193 GatewayExtPayStates::Success { preimage, .. } => {
1194 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1195 return Ok(preimage);
1196 }
1197 GatewayExtPayStates::Fail {
1198 error,
1199 error_message,
1200 } => {
1201 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1202 error: Box::new(error),
1203 message: format!(
1204 "{error_message} while paying invoice with contract id {contract_id}"
1205 ),
1206 }));
1207 }
1208 GatewayExtPayStates::Canceled { error } => {
1209 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1210 error: Box::new(error.clone()),
1211 message: format!(
1212 "Cancelled with {error} while paying invoice with contract id {contract_id}"
1213 ),
1214 }));
1215 }
1216 GatewayExtPayStates::Created => {
1217 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1218 }
1219 other => {
1220 debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1221 }
1222 }
1223 }
1224
1225 Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1226 anyhow!("Ran out of state updates while paying invoice"),
1227 )))
1228 }
1229
1230 pub async fn handle_backup_msg(
1233 &self,
1234 BackupPayload { federation_id }: BackupPayload,
1235 ) -> AdminResult<()> {
1236 let federation_manager = self.federation_manager.read().await;
1237 let client = federation_manager
1238 .client(&federation_id)
1239 .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1240 format!("Gateway has not connected to {federation_id}")
1241 )))?
1242 .value();
1243 let metadata: BTreeMap<String, String> = BTreeMap::new();
1244 #[allow(deprecated)]
1245 client
1246 .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1247 metadata,
1248 ))
1249 .await?;
1250 Ok(())
1251 }
1252
1253 pub async fn handle_recheck_address_msg(
1255 &self,
1256 payload: DepositAddressRecheckPayload,
1257 ) -> AdminResult<()> {
1258 self.select_client(payload.federation_id)
1259 .await?
1260 .value()
1261 .get_first_module::<WalletClientModule>()
1262 .expect("Must have client module")
1263 .recheck_pegin_address_by_address(payload.address)
1264 .await?;
1265 Ok(())
1266 }
1267
1268 pub async fn handle_receive_ecash_msg(
1270 &self,
1271 payload: ReceiveEcashPayload,
1272 ) -> Result<ReceiveEcashResponse> {
1273 let amount = payload.notes.total_amount();
1274 let client = self
1275 .federation_manager
1276 .read()
1277 .await
1278 .get_client_for_federation_id_prefix(payload.notes.federation_id_prefix())
1279 .ok_or(FederationNotConnected {
1280 federation_id_prefix: payload.notes.federation_id_prefix(),
1281 })?;
1282 let mint = client
1283 .value()
1284 .get_first_module::<MintClientModule>()
1285 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1286 failure_reason: format!("Mint module does not exist: {e:?}"),
1287 })?;
1288
1289 let operation_id = mint
1290 .reissue_external_notes(payload.notes, ())
1291 .await
1292 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1293 failure_reason: e.to_string(),
1294 })?;
1295 if payload.wait {
1296 let mut updates = mint
1297 .subscribe_reissue_external_notes(operation_id)
1298 .await
1299 .unwrap()
1300 .into_stream();
1301
1302 while let Some(update) = updates.next().await {
1303 if let fedimint_mint_client::ReissueExternalNotesState::Failed(e) = update {
1304 return Err(PublicGatewayError::ReceiveEcashError {
1305 failure_reason: e.to_string(),
1306 });
1307 }
1308 }
1309 }
1310
1311 Ok(ReceiveEcashResponse { amount })
1312 }
1313
1314 pub async fn handle_get_invoice_msg(
1317 &self,
1318 payload: GetInvoiceRequest,
1319 ) -> AdminResult<Option<GetInvoiceResponse>> {
1320 let lightning_context = self.get_lightning_context().await?;
1321 let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1322 Ok(invoice)
1323 }
1324
1325 async fn register_federations(
1327 &self,
1328 federations: &BTreeMap<FederationId, FederationConfig>,
1329 register_task_group: &TaskGroup,
1330 ) {
1331 if let Ok(lightning_context) = self.get_lightning_context().await {
1332 let route_hints = lightning_context
1333 .lnrpc
1334 .parsed_route_hints(self.num_route_hints)
1335 .await;
1336 if route_hints.is_empty() {
1337 warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
1338 }
1339
1340 for (federation_id, federation_config) in federations {
1341 let fed_manager = self.federation_manager.read().await;
1342 if let Some(client) = fed_manager.client(federation_id) {
1343 let client_arc = client.clone().into_value();
1344 let route_hints = route_hints.clone();
1345 let lightning_context = lightning_context.clone();
1346 let federation_config = federation_config.clone();
1347 let registrations =
1348 self.registrations.clone().into_values().collect::<Vec<_>>();
1349
1350 register_task_group.spawn_cancellable_silent(
1351 "register federation",
1352 async move {
1353 let Ok(gateway_client) =
1354 client_arc.get_first_module::<GatewayClientModule>()
1355 else {
1356 return;
1357 };
1358
1359 for registration in registrations {
1360 gateway_client
1361 .try_register_with_federation(
1362 route_hints.clone(),
1363 GW_ANNOUNCEMENT_TTL,
1364 federation_config.lightning_fee.into(),
1365 lightning_context.clone(),
1366 registration.endpoint_url,
1367 registration.keypair.public_key(),
1368 )
1369 .await;
1370 }
1371 },
1372 );
1373 }
1374 }
1375 }
1376 }
1377
1378 pub async fn select_client(
1381 &self,
1382 federation_id: FederationId,
1383 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
1384 {
1385 self.federation_manager
1386 .read()
1387 .await
1388 .client(&federation_id)
1389 .cloned()
1390 .ok_or(FederationNotConnected {
1391 federation_id_prefix: federation_id.to_prefix(),
1392 })
1393 }
1394
1395 async fn load_mnemonic(gateway_db: &Database) -> Option<Mnemonic> {
1396 let secret = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db)
1397 .await
1398 .ok()?;
1399 Mnemonic::from_entropy(&secret).ok()
1400 }
1401
1402 async fn load_clients(&self) -> AdminResult<()> {
1406 if let GatewayState::NotConfigured { .. } = self.get_state().await {
1407 return Ok(());
1408 }
1409
1410 let mut federation_manager = self.federation_manager.write().await;
1411
1412 let configs = {
1413 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1414 dbtx.load_federation_configs().await
1415 };
1416
1417 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
1418 federation_manager.set_next_index(max_federation_index + 1);
1419 }
1420
1421 let mnemonic = Self::load_mnemonic(&self.gateway_db)
1422 .await
1423 .expect("mnemonic should be set");
1424
1425 for (federation_id, config) in configs {
1426 let federation_index = config.federation_index;
1427 match Box::pin(Spanned::try_new(
1428 info_span!(target: LOG_GATEWAY, "client", federation_id = %federation_id.clone()),
1429 self.client_builder
1430 .build(config, Arc::new(self.clone()), &mnemonic),
1431 ))
1432 .await
1433 {
1434 Ok(client) => {
1435 federation_manager.add_client(federation_index, client);
1436 }
1437 _ => {
1438 warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
1439 }
1440 }
1441 }
1442
1443 Ok(())
1444 }
1445
1446 fn register_clients_timer(&self) {
1452 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1454 info!(target: LOG_GATEWAY, "Spawning register task...");
1455 let gateway = self.clone();
1456 let register_task_group = self.task_group.make_subgroup();
1457 self.task_group.spawn_cancellable("register clients", async move {
1458 loop {
1459 let gateway_state = gateway.get_state().await;
1460 if let GatewayState::Running { .. } = &gateway_state {
1461 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
1462 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
1463 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
1464 } else {
1465 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
1467 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");
1468 sleep(NOT_RUNNING_RETRY).await;
1469 continue;
1470 }
1471
1472 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
1475 }
1476 });
1477 }
1478 }
1479
1480 async fn check_federation_network(
1483 client: &ClientHandleArc,
1484 network: Network,
1485 ) -> AdminResult<()> {
1486 let federation_id = client.federation_id();
1487 let config = client.config().await;
1488
1489 let lnv1_cfg = config
1490 .modules
1491 .values()
1492 .find(|m| LightningCommonInit::KIND == m.kind);
1493
1494 let lnv2_cfg = config
1495 .modules
1496 .values()
1497 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
1498
1499 if lnv1_cfg.is_none() && lnv2_cfg.is_none() {
1501 return Err(AdminGatewayError::ClientCreationError(anyhow!(
1502 "Federation {federation_id} does not have any lightning module (LNv1 or LNv2)"
1503 )));
1504 }
1505
1506 if let Some(cfg) = lnv1_cfg {
1508 let ln_cfg: &LightningClientConfig = cfg.cast()?;
1509
1510 if ln_cfg.network.0 != network {
1511 crit!(
1512 target: LOG_GATEWAY,
1513 federation_id = %federation_id,
1514 network = %network,
1515 "Incorrect LNv1 network for federation",
1516 );
1517 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1518 "Unsupported LNv1 network {}",
1519 ln_cfg.network
1520 ))));
1521 }
1522 }
1523
1524 if let Some(cfg) = lnv2_cfg {
1526 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig = cfg.cast()?;
1527
1528 if ln_cfg.network != network {
1529 crit!(
1530 target: LOG_GATEWAY,
1531 federation_id = %federation_id,
1532 network = %network,
1533 "Incorrect LNv2 network for federation",
1534 );
1535 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1536 "Unsupported LNv2 network {}",
1537 ln_cfg.network
1538 ))));
1539 }
1540 }
1541
1542 Ok(())
1543 }
1544
1545 pub async fn get_lightning_context(
1549 &self,
1550 ) -> std::result::Result<LightningContext, LightningRpcError> {
1551 match self.get_state().await {
1552 GatewayState::Running { lightning_context }
1553 | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
1554 _ => Err(LightningRpcError::FailedToConnect),
1555 }
1556 }
1557
1558 pub async fn unannounce_from_all_federations(&self) {
1561 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1562 for registration in self.registrations.values() {
1563 self.federation_manager
1564 .read()
1565 .await
1566 .unannounce_from_all_federations(registration.keypair)
1567 .await;
1568 }
1569 }
1570 }
1571
1572 async fn create_lightning_client(
1573 &self,
1574 runtime: Arc<tokio::runtime::Runtime>,
1575 ) -> Box<dyn ILnRpcClient> {
1576 match self.lightning_mode.clone() {
1577 LightningMode::Lnd {
1578 lnd_rpc_addr,
1579 lnd_tls_cert,
1580 lnd_macaroon,
1581 } => Box::new(GatewayLndClient::new(
1582 lnd_rpc_addr,
1583 lnd_tls_cert,
1584 lnd_macaroon,
1585 None,
1586 )),
1587 LightningMode::Ldk {
1588 lightning_port,
1589 alias,
1590 } => {
1591 let mnemonic = Self::load_mnemonic(&self.gateway_db)
1592 .await
1593 .expect("mnemonic should be set");
1594 retry("create LDK Node", fibonacci_max_one_hour(), || async {
1598 ldk::GatewayLdkClient::new(
1599 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
1600 self.chain_source.clone(),
1601 self.network,
1602 lightning_port,
1603 alias.clone(),
1604 mnemonic.clone(),
1605 runtime.clone(),
1606 )
1607 .map(Box::new)
1608 })
1609 .await
1610 .expect("Could not create LDK Node")
1611 }
1612 }
1613 }
1614}
1615
1616#[async_trait]
1617impl IAdminGateway for Gateway {
1618 type Error = AdminGatewayError;
1619
1620 async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
1623 let GatewayState::Running { lightning_context } = self.get_state().await else {
1624 return Ok(GatewayInfo {
1625 federations: vec![],
1626 federation_fake_scids: None,
1627 version_hash: fedimint_build_code_version_env!().to_string(),
1628 gateway_state: self.state.read().await.to_string(),
1629 lightning_info: LightningInfo::NotConnected,
1630 lightning_mode: self.lightning_mode.clone(),
1631 registrations: self
1632 .registrations
1633 .iter()
1634 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
1635 .collect(),
1636 });
1637 };
1638
1639 let dbtx = self.gateway_db.begin_transaction_nc().await;
1640 let federations = self
1641 .federation_manager
1642 .read()
1643 .await
1644 .federation_info_all_federations(dbtx)
1645 .await;
1646
1647 let channels: BTreeMap<u64, FederationId> = federations
1648 .iter()
1649 .map(|federation_info| {
1650 (
1651 federation_info.config.federation_index,
1652 federation_info.federation_id,
1653 )
1654 })
1655 .collect();
1656
1657 let lightning_info = lightning_context.lnrpc.parsed_node_info().await;
1658
1659 Ok(GatewayInfo {
1660 federations,
1661 federation_fake_scids: Some(channels),
1662 version_hash: fedimint_build_code_version_env!().to_string(),
1663 gateway_state: self.state.read().await.to_string(),
1664 lightning_info,
1665 lightning_mode: self.lightning_mode.clone(),
1666 registrations: self
1667 .registrations
1668 .iter()
1669 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
1670 .collect(),
1671 })
1672 }
1673
1674 async fn handle_list_channels_msg(
1677 &self,
1678 ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
1679 let context = self.get_lightning_context().await?;
1680 let response = context.lnrpc.list_channels().await?;
1681 Ok(response.channels)
1682 }
1683
1684 async fn handle_payment_summary_msg(
1687 &self,
1688 PaymentSummaryPayload {
1689 start_millis,
1690 end_millis,
1691 }: PaymentSummaryPayload,
1692 ) -> AdminResult<PaymentSummaryResponse> {
1693 let federation_manager = self.federation_manager.read().await;
1694 let fed_configs = federation_manager.get_all_federation_configs().await;
1695 let federation_ids = fed_configs.keys().collect::<Vec<_>>();
1696 let start = UNIX_EPOCH + Duration::from_millis(start_millis);
1697 let end = UNIX_EPOCH + Duration::from_millis(end_millis);
1698
1699 if start > end {
1700 return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
1701 }
1702
1703 let mut outgoing = StructuredPaymentEvents::default();
1704 let mut incoming = StructuredPaymentEvents::default();
1705 for fed_id in federation_ids {
1706 let client = federation_manager
1707 .client(fed_id)
1708 .expect("No client available")
1709 .value();
1710 let all_events = &get_events_for_duration(client, start, end).await;
1711
1712 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
1713 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
1714 outgoing.combine(&mut lnv1_outgoing);
1715 incoming.combine(&mut lnv1_incoming);
1716 outgoing.combine(&mut lnv2_outgoing);
1717 incoming.combine(&mut lnv2_incoming);
1718 }
1719
1720 Ok(PaymentSummaryResponse {
1721 outgoing: PaymentStats::compute(&outgoing),
1722 incoming: PaymentStats::compute(&incoming),
1723 })
1724 }
1725
1726 async fn handle_leave_federation(
1731 &self,
1732 payload: LeaveFedPayload,
1733 ) -> AdminResult<FederationInfo> {
1734 let mut federation_manager = self.federation_manager.write().await;
1737 let mut dbtx = self.gateway_db.begin_transaction().await;
1738
1739 let federation_info = federation_manager
1740 .leave_federation(
1741 payload.federation_id,
1742 &mut dbtx.to_ref_nc(),
1743 self.registrations.values().collect(),
1744 )
1745 .await?;
1746
1747 dbtx.remove_federation_config(payload.federation_id).await;
1748 dbtx.commit_tx().await;
1749 Ok(federation_info)
1750 }
1751
1752 async fn handle_connect_federation(
1757 &self,
1758 payload: ConnectFedPayload,
1759 ) -> AdminResult<FederationInfo> {
1760 let GatewayState::Running { lightning_context } = self.get_state().await else {
1761 return Err(AdminGatewayError::Lightning(
1762 LightningRpcError::FailedToConnect,
1763 ));
1764 };
1765
1766 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
1767 AdminGatewayError::ClientCreationError(anyhow!(format!(
1768 "Invalid federation member string {e:?}"
1769 )))
1770 })?;
1771
1772 let federation_id = invite_code.federation_id();
1773
1774 let mut federation_manager = self.federation_manager.write().await;
1775
1776 if federation_manager.has_federation(federation_id) {
1778 return Err(AdminGatewayError::ClientCreationError(anyhow!(
1779 "Federation has already been registered"
1780 )));
1781 }
1782
1783 let federation_index = federation_manager.pop_next_index()?;
1786
1787 let federation_config = FederationConfig {
1788 invite_code,
1789 federation_index,
1790 lightning_fee: self.default_routing_fees,
1791 transaction_fee: self.default_transaction_fees,
1792 _connector: ConnectorType::Tcp,
1794 };
1795
1796 let mnemonic = Self::load_mnemonic(&self.gateway_db)
1797 .await
1798 .expect("mnemonic should be set");
1799 let recover = payload.recover.unwrap_or(false);
1800 if recover {
1801 self.client_builder
1802 .recover(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
1803 .await?;
1804 }
1805
1806 let client = self
1807 .client_builder
1808 .build(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
1809 .await?;
1810
1811 if recover {
1812 client.wait_for_all_active_state_machines().await?;
1813 }
1814
1815 let federation_info = FederationInfo {
1818 federation_id,
1819 federation_name: federation_manager.federation_name(&client).await,
1820 balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
1821 warn!(
1822 target: LOG_GATEWAY,
1823 err = %err.fmt_compact_anyhow(),
1824 %federation_id,
1825 "Balance not immediately available after joining/recovering."
1826 );
1827 Amount::default()
1828 }),
1829 config: federation_config.clone(),
1830 last_backup_time: None,
1831 };
1832
1833 Self::check_federation_network(&client, self.network).await?;
1834 if matches!(self.lightning_mode, LightningMode::Lnd { .. })
1835 && let Ok(lnv1) = client.get_first_module::<GatewayClientModule>()
1836 {
1837 for registration in self.registrations.values() {
1838 lnv1.try_register_with_federation(
1839 Vec::new(),
1841 GW_ANNOUNCEMENT_TTL,
1842 federation_config.lightning_fee.into(),
1843 lightning_context.clone(),
1844 registration.endpoint_url.clone(),
1845 registration.keypair.public_key(),
1846 )
1847 .await;
1848 }
1849 }
1850
1851 federation_manager.add_client(
1853 federation_index,
1854 Spanned::new(
1855 info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
1856 async { client },
1857 )
1858 .await,
1859 );
1860
1861 let mut dbtx = self.gateway_db.begin_transaction().await;
1862 dbtx.save_federation_config(&federation_config).await;
1863 dbtx.save_federation_backup_record(federation_id, None)
1864 .await;
1865 dbtx.commit_tx().await;
1866 debug!(
1867 target: LOG_GATEWAY,
1868 federation_id = %federation_id,
1869 federation_index = %federation_index,
1870 "Federation connected"
1871 );
1872
1873 Ok(federation_info)
1874 }
1875
1876 async fn handle_set_fees_msg(
1879 &self,
1880 SetFeesPayload {
1881 federation_id,
1882 lightning_base,
1883 lightning_parts_per_million,
1884 transaction_base,
1885 transaction_parts_per_million,
1886 }: SetFeesPayload,
1887 ) -> AdminResult<()> {
1888 let mut dbtx = self.gateway_db.begin_transaction().await;
1889 let mut fed_configs = if let Some(fed_id) = federation_id {
1890 dbtx.load_federation_configs()
1891 .await
1892 .into_iter()
1893 .filter(|(id, _)| *id == fed_id)
1894 .collect::<BTreeMap<_, _>>()
1895 } else {
1896 dbtx.load_federation_configs().await
1897 };
1898
1899 let federation_manager = self.federation_manager.read().await;
1900
1901 for (federation_id, config) in &mut fed_configs {
1902 let mut lightning_fee = config.lightning_fee;
1903 if let Some(lightning_base) = lightning_base {
1904 lightning_fee.base = lightning_base;
1905 }
1906
1907 if let Some(lightning_ppm) = lightning_parts_per_million {
1908 lightning_fee.parts_per_million = lightning_ppm;
1909 }
1910
1911 let mut transaction_fee = config.transaction_fee;
1912 if let Some(transaction_base) = transaction_base {
1913 transaction_fee.base = transaction_base;
1914 }
1915
1916 if let Some(transaction_ppm) = transaction_parts_per_million {
1917 transaction_fee.parts_per_million = transaction_ppm;
1918 }
1919
1920 let client =
1921 federation_manager
1922 .client(federation_id)
1923 .ok_or(FederationNotConnected {
1924 federation_id_prefix: federation_id.to_prefix(),
1925 })?;
1926 let client_config = client.value().config().await;
1927 let contains_lnv2 = client_config
1928 .modules
1929 .values()
1930 .any(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
1931
1932 let send_fees = lightning_fee + transaction_fee;
1934 if contains_lnv2 && send_fees.gt(&PaymentFee::SEND_FEE_LIMIT) {
1935 return Err(AdminGatewayError::GatewayConfigurationError(format!(
1936 "Total Send fees exceeded {}",
1937 PaymentFee::SEND_FEE_LIMIT
1938 )));
1939 }
1940
1941 if contains_lnv2 && transaction_fee.gt(&PaymentFee::RECEIVE_FEE_LIMIT) {
1943 return Err(AdminGatewayError::GatewayConfigurationError(format!(
1944 "Transaction fees exceeded RECEIVE LIMIT {}",
1945 PaymentFee::RECEIVE_FEE_LIMIT
1946 )));
1947 }
1948
1949 config.lightning_fee = lightning_fee;
1950 config.transaction_fee = transaction_fee;
1951 dbtx.save_federation_config(config).await;
1952 }
1953
1954 dbtx.commit_tx().await;
1955
1956 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1957 let register_task_group = TaskGroup::new();
1958
1959 self.register_federations(&fed_configs, ®ister_task_group)
1960 .await;
1961 }
1962
1963 Ok(())
1964 }
1965
1966 async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
1970 let mnemonic = Self::load_mnemonic(&self.gateway_db)
1971 .await
1972 .expect("mnemonic should be set");
1973 let words = mnemonic
1974 .words()
1975 .map(std::string::ToString::to_string)
1976 .collect::<Vec<_>>();
1977 let all_federations = self
1978 .federation_manager
1979 .read()
1980 .await
1981 .get_all_federation_configs()
1982 .await
1983 .keys()
1984 .copied()
1985 .collect::<BTreeSet<_>>();
1986 let legacy_federations = self.client_builder.legacy_federations(all_federations);
1987 let mnemonic_response = MnemonicResponse {
1988 mnemonic: words,
1989 legacy_federations,
1990 };
1991 Ok(mnemonic_response)
1992 }
1993
1994 async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
1997 info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
1998 let context = self.get_lightning_context().await?;
1999 let res = context.lnrpc.open_channel(payload).await?;
2000 info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
2001 Txid::from_str(&res.funding_txid).map_err(|e| {
2002 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2003 failure_reason: format!("Received invalid channel funding txid string {e}"),
2004 })
2005 })
2006 }
2007
2008 async fn handle_close_channels_with_peer_msg(
2011 &self,
2012 payload: CloseChannelsWithPeerRequest,
2013 ) -> AdminResult<CloseChannelsWithPeerResponse> {
2014 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Closing lightning channel...");
2015 let context = self.get_lightning_context().await?;
2016 let response = context
2017 .lnrpc
2018 .close_channels_with_peer(payload.clone())
2019 .await?;
2020 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Initiated channel closure");
2021 Ok(response)
2022 }
2023
2024 async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
2027 let dbtx = self.gateway_db.begin_transaction_nc().await;
2028 let federation_infos = self
2029 .federation_manager
2030 .read()
2031 .await
2032 .federation_info_all_federations(dbtx)
2033 .await;
2034
2035 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
2036 .iter()
2037 .map(|federation_info| FederationBalanceInfo {
2038 federation_id: federation_info.federation_id,
2039 ecash_balance_msats: Amount {
2040 msats: federation_info.balance_msat.msats,
2041 },
2042 })
2043 .collect();
2044
2045 let context = self.get_lightning_context().await?;
2046 let lightning_node_balances = context.lnrpc.get_balances().await?;
2047
2048 Ok(GatewayBalances {
2049 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
2050 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
2051 ecash_balances,
2052 inbound_lightning_liquidity_msats: lightning_node_balances
2053 .inbound_lightning_liquidity_msats,
2054 })
2055 }
2056
2057 async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
2059 let context = self.get_lightning_context().await?;
2060 let response = context.lnrpc.send_onchain(payload.clone()).await?;
2061 let txid =
2062 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
2063 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
2064 })?;
2065 info!(onchain_request = %payload, txid = %txid, "Sent onchain transaction");
2066 Ok(txid)
2067 }
2068
2069 async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
2071 let context = self.get_lightning_context().await?;
2072 let response = context.lnrpc.get_ln_onchain_address().await?;
2073
2074 let address = Address::from_str(&response.address).map_err(|e| {
2075 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2076 failure_reason: e.to_string(),
2077 })
2078 })?;
2079
2080 address.require_network(self.network).map_err(|e| {
2081 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2082 failure_reason: e.to_string(),
2083 })
2084 })
2085 }
2086
2087 async fn handle_deposit_address_msg(
2088 &self,
2089 payload: DepositAddressPayload,
2090 ) -> AdminResult<Address> {
2091 self.handle_address_msg(payload).await
2092 }
2093
2094 async fn handle_receive_ecash_msg(
2095 &self,
2096 payload: ReceiveEcashPayload,
2097 ) -> AdminResult<ReceiveEcashResponse> {
2098 Self::handle_receive_ecash_msg(self, payload)
2099 .await
2100 .map_err(|e| AdminGatewayError::Unexpected(anyhow::anyhow!("{}", e)))
2101 }
2102
2103 async fn handle_create_invoice_for_operator_msg(
2106 &self,
2107 payload: CreateInvoiceForOperatorPayload,
2108 ) -> AdminResult<Bolt11Invoice> {
2109 let GatewayState::Running { lightning_context } = self.get_state().await else {
2110 return Err(AdminGatewayError::Lightning(
2111 LightningRpcError::FailedToConnect,
2112 ));
2113 };
2114
2115 Bolt11Invoice::from_str(
2116 &lightning_context
2117 .lnrpc
2118 .create_invoice(CreateInvoiceRequest {
2119 payment_hash: None, amount_msat: payload.amount_msats,
2122 expiry_secs: payload.expiry_secs.unwrap_or(3600),
2123 description: payload.description.map(InvoiceDescription::Direct),
2124 })
2125 .await?
2126 .invoice,
2127 )
2128 .map_err(|e| {
2129 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2130 failure_reason: e.to_string(),
2131 })
2132 })
2133 }
2134
2135 async fn handle_pay_invoice_for_operator_msg(
2138 &self,
2139 payload: PayInvoiceForOperatorPayload,
2140 ) -> AdminResult<Preimage> {
2141 const BASE_FEE: u64 = 50;
2143 const FEE_DENOMINATOR: u64 = 100;
2144 const MAX_DELAY: u64 = 1008;
2145
2146 let GatewayState::Running { lightning_context } = self.get_state().await else {
2147 return Err(AdminGatewayError::Lightning(
2148 LightningRpcError::FailedToConnect,
2149 ));
2150 };
2151
2152 let max_fee = BASE_FEE
2153 + payload
2154 .invoice
2155 .amount_milli_satoshis()
2156 .context("Invoice is missing amount")?
2157 .saturating_div(FEE_DENOMINATOR);
2158
2159 let res = lightning_context
2160 .lnrpc
2161 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
2162 .await?;
2163 Ok(res.preimage)
2164 }
2165
2166 async fn handle_list_transactions_msg(
2168 &self,
2169 payload: ListTransactionsPayload,
2170 ) -> AdminResult<ListTransactionsResponse> {
2171 let lightning_context = self.get_lightning_context().await?;
2172 let response = lightning_context
2173 .lnrpc
2174 .list_transactions(payload.start_secs, payload.end_secs)
2175 .await?;
2176 Ok(response)
2177 }
2178
2179 async fn handle_spend_ecash_msg(
2181 &self,
2182 payload: SpendEcashPayload,
2183 ) -> AdminResult<SpendEcashResponse> {
2184 let client = self
2185 .select_client(payload.federation_id)
2186 .await?
2187 .into_value();
2188 let mint_module = client.get_first_module::<MintClientModule>()?;
2189 let timeout = Duration::from_secs(payload.timeout);
2190 let (operation_id, notes) = if payload.allow_overpay {
2191 let (operation_id, notes) = mint_module
2192 .spend_notes_with_selector(
2193 &SelectNotesWithAtleastAmount,
2194 payload.amount,
2195 timeout,
2196 payload.include_invite,
2197 (),
2198 )
2199 .await?;
2200
2201 let overspend_amount = notes.total_amount().saturating_sub(payload.amount);
2202 if overspend_amount != Amount::ZERO {
2203 warn!(
2204 target: LOG_GATEWAY,
2205 overspend_amount = %overspend_amount,
2206 "Selected notes worth more than requested",
2207 );
2208 }
2209
2210 (operation_id, notes)
2211 } else {
2212 mint_module
2213 .spend_notes_with_selector(
2214 &SelectNotesWithExactAmount,
2215 payload.amount,
2216 timeout,
2217 payload.include_invite,
2218 (),
2219 )
2220 .await?
2221 };
2222
2223 debug!(target: LOG_GATEWAY, ?operation_id, ?notes, "Spend ecash notes");
2224
2225 Ok(SpendEcashResponse {
2226 operation_id,
2227 notes,
2228 })
2229 }
2230
2231 async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
2234 let mut state_guard = self.state.write().await;
2236 if let GatewayState::Running { lightning_context } = state_guard.clone() {
2237 *state_guard = GatewayState::ShuttingDown { lightning_context };
2238
2239 self.federation_manager
2240 .read()
2241 .await
2242 .wait_for_incoming_payments()
2243 .await?;
2244 }
2245
2246 let tg = task_group.clone();
2247 tg.spawn("Kill Gateway", |_task_handle| async {
2248 if let Err(err) = task_group.shutdown_join_all(Duration::from_secs(180)).await {
2249 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error shutting down gateway");
2250 }
2251 });
2252 Ok(())
2253 }
2254
2255 fn get_task_group(&self) -> TaskGroup {
2256 self.task_group.clone()
2257 }
2258
2259 async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> AdminResult<WithdrawResponse> {
2262 let WithdrawPayload {
2263 amount,
2264 address,
2265 federation_id,
2266 quoted_fees,
2267 } = payload;
2268
2269 let address_network = get_network_for_address(&address);
2270 let gateway_network = self.network;
2271 let Ok(address) = address.require_network(gateway_network) else {
2272 return Err(AdminGatewayError::WithdrawError {
2273 failure_reason: format!(
2274 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
2275 ),
2276 });
2277 };
2278
2279 let client = self.select_client(federation_id).await?;
2280 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
2281
2282 let (withdraw_amount, fees) = match quoted_fees {
2285 Some(fees) => {
2287 let amt = match amount {
2288 BitcoinAmountOrAll::Amount(a) => a,
2289 BitcoinAmountOrAll::All => {
2290 return Err(AdminGatewayError::WithdrawError {
2292 failure_reason:
2293 "Cannot use 'all' with quoted fees - amount must be resolved first"
2294 .to_string(),
2295 });
2296 }
2297 };
2298 (amt, fees)
2299 }
2300 None => match amount {
2302 BitcoinAmountOrAll::All => {
2305 let balance = bitcoin::Amount::from_sat(
2306 client
2307 .value()
2308 .get_balance_for_btc()
2309 .await
2310 .map_err(|err| {
2311 AdminGatewayError::Unexpected(anyhow!(
2312 "Balance not available: {}",
2313 err.fmt_compact_anyhow()
2314 ))
2315 })?
2316 .msats
2317 / 1000,
2318 );
2319 let fees = wallet_module.get_withdraw_fees(&address, balance).await?;
2320 let withdraw_amount = balance.checked_sub(fees.amount());
2321 if withdraw_amount.is_none() {
2322 return Err(AdminGatewayError::WithdrawError {
2323 failure_reason: format!(
2324 "Insufficient funds. Balance: {balance} Fees: {fees:?}"
2325 ),
2326 });
2327 }
2328 (withdraw_amount.expect("checked above"), fees)
2329 }
2330 BitcoinAmountOrAll::Amount(amount) => (
2331 amount,
2332 wallet_module.get_withdraw_fees(&address, amount).await?,
2333 ),
2334 },
2335 };
2336
2337 let operation_id = wallet_module
2338 .withdraw(&address, withdraw_amount, fees, ())
2339 .await?;
2340 let mut updates = wallet_module
2341 .subscribe_withdraw_updates(operation_id)
2342 .await?
2343 .into_stream();
2344
2345 while let Some(update) = updates.next().await {
2346 match update {
2347 WithdrawState::Succeeded(txid) => {
2348 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds");
2349 return Ok(WithdrawResponse { txid, fees });
2350 }
2351 WithdrawState::Failed(e) => {
2352 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
2353 }
2354 WithdrawState::Created => {}
2355 }
2356 }
2357
2358 Err(AdminGatewayError::WithdrawError {
2359 failure_reason: "Ran out of state updates while withdrawing".to_string(),
2360 })
2361 }
2362
2363 async fn handle_withdraw_preview_msg(
2366 &self,
2367 payload: WithdrawPreviewPayload,
2368 ) -> AdminResult<WithdrawPreviewResponse> {
2369 let gateway_network = self.network;
2370 let address_checked = payload
2371 .address
2372 .clone()
2373 .require_network(gateway_network)
2374 .map_err(|_| AdminGatewayError::WithdrawError {
2375 failure_reason: "Address network mismatch".to_string(),
2376 })?;
2377
2378 let client = self.select_client(payload.federation_id).await?;
2379 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
2380
2381 let WithdrawDetails {
2382 amount,
2383 mint_fees,
2384 peg_out_fees,
2385 } = match payload.amount {
2386 BitcoinAmountOrAll::All => {
2387 calculate_max_withdrawable(client.value(), &address_checked).await?
2388 }
2389 BitcoinAmountOrAll::Amount(btc_amount) => WithdrawDetails {
2390 amount: btc_amount.into(),
2391 mint_fees: None,
2392 peg_out_fees: wallet_module
2393 .get_withdraw_fees(&address_checked, btc_amount)
2394 .await?,
2395 },
2396 };
2397
2398 let total_cost = amount
2399 .checked_add(peg_out_fees.amount().into())
2400 .and_then(|a| a.checked_add(mint_fees.unwrap_or(Amount::ZERO)))
2401 .ok_or_else(|| AdminGatewayError::Unexpected(anyhow!("Total cost overflow")))?;
2402
2403 Ok(WithdrawPreviewResponse {
2404 withdraw_amount: amount,
2405 address: payload.address.assume_checked().to_string(),
2406 peg_out_fees,
2407 total_cost,
2408 mint_fees,
2409 })
2410 }
2411
2412 async fn handle_payment_log_msg(
2414 &self,
2415 PaymentLogPayload {
2416 end_position,
2417 pagination_size,
2418 federation_id,
2419 event_kinds,
2420 }: PaymentLogPayload,
2421 ) -> AdminResult<PaymentLogResponse> {
2422 const BATCH_SIZE: u64 = 10_000;
2423 let federation_manager = self.federation_manager.read().await;
2424 let client = federation_manager
2425 .client(&federation_id)
2426 .ok_or(FederationNotConnected {
2427 federation_id_prefix: federation_id.to_prefix(),
2428 })?
2429 .value();
2430
2431 let event_kinds = if event_kinds.is_empty() {
2432 ALL_GATEWAY_EVENTS.to_vec()
2433 } else {
2434 event_kinds
2435 };
2436
2437 let end_position = if let Some(position) = end_position {
2438 position
2439 } else {
2440 let mut dbtx = client.db().begin_transaction_nc().await;
2441 dbtx.get_next_event_log_id().await
2442 };
2443
2444 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
2445
2446 let mut payment_log = Vec::new();
2447
2448 while payment_log.len() < pagination_size {
2449 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
2450 let mut filtered_batch = batch
2451 .into_iter()
2452 .filter(|e| e.id() <= end_position && event_kinds.contains(&e.as_raw().kind))
2453 .collect::<Vec<_>>();
2454 filtered_batch.reverse();
2455 payment_log.extend(filtered_batch);
2456
2457 start_position = start_position.saturating_sub(BATCH_SIZE);
2459
2460 if start_position == EventLogId::LOG_START {
2461 break;
2462 }
2463 }
2464
2465 payment_log.truncate(pagination_size);
2467
2468 Ok(PaymentLogResponse(payment_log))
2469 }
2470
2471 async fn handle_set_mnemonic_msg(&self, payload: SetMnemonicPayload) -> AdminResult<()> {
2474 let GatewayState::NotConfigured { mnemonic_sender } = self.get_state().await else {
2476 return Err(AdminGatewayError::MnemonicError(anyhow!(
2477 "Gateway is not is NotConfigured state"
2478 )));
2479 };
2480
2481 let mnemonic = if let Some(words) = payload.words {
2482 info!(target: LOG_GATEWAY, "Using user provided mnemonic");
2483 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(|e| {
2484 AdminGatewayError::MnemonicError(anyhow!(format!(
2485 "Seed phrase provided in environment was invalid {e:?}"
2486 )))
2487 })?
2488 } else {
2489 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
2490 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
2491 };
2492
2493 Client::store_encodable_client_secret(&self.gateway_db, mnemonic.to_entropy())
2494 .await
2495 .map_err(AdminGatewayError::MnemonicError)?;
2496
2497 self.set_gateway_state(GatewayState::Disconnected).await;
2498
2499 let _ = mnemonic_sender.send(());
2501
2502 Ok(())
2503 }
2504
2505 async fn handle_create_offer_for_operator_msg(
2507 &self,
2508 payload: CreateOfferPayload,
2509 ) -> AdminResult<CreateOfferResponse> {
2510 let lightning_context = self.get_lightning_context().await?;
2511 let offer = lightning_context.lnrpc.create_offer(
2512 payload.amount,
2513 payload.description,
2514 payload.expiry_secs,
2515 payload.quantity,
2516 )?;
2517 Ok(CreateOfferResponse { offer })
2518 }
2519
2520 async fn handle_pay_offer_for_operator_msg(
2522 &self,
2523 payload: PayOfferPayload,
2524 ) -> AdminResult<PayOfferResponse> {
2525 let lightning_context = self.get_lightning_context().await?;
2526 let preimage = lightning_context
2527 .lnrpc
2528 .pay_offer(
2529 payload.offer,
2530 payload.quantity,
2531 payload.amount,
2532 payload.payer_note,
2533 )
2534 .await?;
2535 Ok(PayOfferResponse {
2536 preimage: preimage.to_string(),
2537 })
2538 }
2539
2540 async fn handle_export_invite_codes(&self) -> BTreeMap<FederationId, Vec<InviteCode>> {
2543 let fed_manager = self.federation_manager.read().await;
2544 fed_manager.all_invite_codes().await
2545 }
2546
2547 fn get_password_hash(&self) -> String {
2548 self.bcrypt_password_hash.to_string()
2549 }
2550
2551 fn gatewayd_version(&self) -> String {
2552 let gatewayd_version = env!("CARGO_PKG_VERSION");
2553 gatewayd_version.to_string()
2554 }
2555
2556 async fn get_chain_source(&self) -> (ChainSource, Network) {
2557 (self.chain_source.clone(), self.network)
2558 }
2559
2560 fn lightning_mode(&self) -> LightningMode {
2561 self.lightning_mode.clone()
2562 }
2563
2564 async fn is_configured(&self) -> bool {
2565 !matches!(self.get_state().await, GatewayState::NotConfigured { .. })
2566 }
2567}
2568
2569impl Gateway {
2571 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
2575 self.federation_manager
2576 .read()
2577 .await
2578 .client(federation_id)
2579 .map(|client| {
2580 client
2581 .value()
2582 .get_first_module::<GatewayClientModuleV2>()
2583 .expect("Must have client module")
2584 .keypair
2585 .public_key()
2586 })
2587 }
2588
2589 pub async fn routing_info_v2(
2592 &self,
2593 federation_id: &FederationId,
2594 ) -> Result<Option<RoutingInfo>> {
2595 let context = self.get_lightning_context().await?;
2596
2597 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2598 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
2599 PublicGatewayError::FederationNotConnected(FederationNotConnected {
2600 federation_id_prefix: federation_id.to_prefix(),
2601 }),
2602 )?;
2603
2604 let lightning_fee = fed_config.lightning_fee;
2605 let transaction_fee = fed_config.transaction_fee;
2606
2607 Ok(self
2608 .public_key_v2(federation_id)
2609 .await
2610 .map(|module_public_key| RoutingInfo {
2611 lightning_public_key: context.lightning_public_key,
2612 module_public_key,
2613 send_fee_default: lightning_fee + transaction_fee,
2614 send_fee_minimum: transaction_fee,
2618 expiration_delta_default: 1440,
2619 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
2620 receive_fee: transaction_fee,
2623 }))
2624 }
2625
2626 async fn send_payment_v2(
2629 &self,
2630 payload: SendPaymentPayload,
2631 ) -> Result<std::result::Result<[u8; 32], Signature>> {
2632 self.select_client(payload.federation_id)
2633 .await?
2634 .value()
2635 .get_first_module::<GatewayClientModuleV2>()
2636 .expect("Must have client module")
2637 .send_payment(payload)
2638 .await
2639 .map_err(LNv2Error::OutgoingPayment)
2640 .map_err(PublicGatewayError::LNv2)
2641 }
2642
2643 async fn create_bolt11_invoice_v2(
2648 &self,
2649 payload: CreateBolt11InvoicePayload,
2650 ) -> Result<Bolt11Invoice> {
2651 if !payload.contract.verify() {
2652 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2653 "The contract is invalid".to_string(),
2654 )));
2655 }
2656
2657 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
2658 LNv2Error::IncomingPayment(format!(
2659 "Federation {} does not exist",
2660 payload.federation_id
2661 )),
2662 )?;
2663
2664 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
2665 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2666 "The incoming contract is keyed to another gateway".to_string(),
2667 )));
2668 }
2669
2670 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
2671
2672 if contract_amount == Amount::ZERO {
2673 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2674 "Zero amount incoming contracts are not supported".to_string(),
2675 )));
2676 }
2677
2678 if contract_amount != payload.contract.commitment.amount {
2679 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2680 "The contract amount does not pay the correct amount of fees".to_string(),
2681 )));
2682 }
2683
2684 if payload.contract.commitment.expiration <= duration_since_epoch().as_secs() {
2685 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2686 "The contract has already expired".to_string(),
2687 )));
2688 }
2689
2690 let payment_hash = match payload.contract.commitment.payment_image {
2691 PaymentImage::Hash(payment_hash) => payment_hash,
2692 PaymentImage::Point(..) => {
2693 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2694 "PaymentImage is not a payment hash".to_string(),
2695 )));
2696 }
2697 };
2698
2699 let invoice = self
2700 .create_invoice_via_lnrpc_v2(
2701 payment_hash,
2702 payload.amount,
2703 payload.description.clone(),
2704 payload.expiry_secs,
2705 )
2706 .await?;
2707
2708 let mut dbtx = self.gateway_db.begin_transaction().await;
2709
2710 if dbtx
2711 .save_registered_incoming_contract(
2712 payload.federation_id,
2713 payload.amount,
2714 payload.contract,
2715 )
2716 .await
2717 .is_some()
2718 {
2719 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2720 "PaymentHash is already registered".to_string(),
2721 )));
2722 }
2723
2724 dbtx.commit_tx_result().await.map_err(|_| {
2725 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2726 "Payment hash is already registered".to_string(),
2727 ))
2728 })?;
2729
2730 Ok(invoice)
2731 }
2732
2733 pub async fn create_invoice_via_lnrpc_v2(
2736 &self,
2737 payment_hash: sha256::Hash,
2738 amount: Amount,
2739 description: Bolt11InvoiceDescription,
2740 expiry_time: u32,
2741 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
2742 let lnrpc = self.get_lightning_context().await?.lnrpc;
2743
2744 let response = match description {
2745 Bolt11InvoiceDescription::Direct(description) => {
2746 lnrpc
2747 .create_invoice(CreateInvoiceRequest {
2748 payment_hash: Some(payment_hash),
2749 amount_msat: amount.msats,
2750 expiry_secs: expiry_time,
2751 description: Some(InvoiceDescription::Direct(description)),
2752 })
2753 .await?
2754 }
2755 Bolt11InvoiceDescription::Hash(hash) => {
2756 lnrpc
2757 .create_invoice(CreateInvoiceRequest {
2758 payment_hash: Some(payment_hash),
2759 amount_msat: amount.msats,
2760 expiry_secs: expiry_time,
2761 description: Some(InvoiceDescription::Hash(hash)),
2762 })
2763 .await?
2764 }
2765 };
2766
2767 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
2768 LightningRpcError::FailedToGetInvoice {
2769 failure_reason: e.to_string(),
2770 }
2771 })
2772 }
2773
2774 pub async fn verify_bolt11_preimage_v2(
2775 &self,
2776 payment_hash: sha256::Hash,
2777 wait: bool,
2778 ) -> std::result::Result<VerifyResponse, String> {
2779 let registered_contract = self
2780 .gateway_db
2781 .begin_transaction_nc()
2782 .await
2783 .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
2784 .await
2785 .ok_or("Unknown payment hash".to_string())?;
2786
2787 let client = self
2788 .select_client(registered_contract.federation_id)
2789 .await
2790 .map_err(|_| "Not connected to federation".to_string())?
2791 .into_value();
2792
2793 let operation_id = OperationId::from_encodable(®istered_contract.contract);
2794
2795 if !(wait || client.operation_exists(operation_id).await) {
2796 return Ok(VerifyResponse {
2797 settled: false,
2798 preimage: None,
2799 });
2800 }
2801
2802 let state = client
2803 .get_first_module::<GatewayClientModuleV2>()
2804 .expect("Must have client module")
2805 .await_receive(operation_id)
2806 .await;
2807
2808 let preimage = match state {
2809 FinalReceiveState::Success(preimage) => Ok(preimage),
2810 FinalReceiveState::Failure => Err("Payment has failed".to_string()),
2811 FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
2812 FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
2813 }?;
2814
2815 Ok(VerifyResponse {
2816 settled: true,
2817 preimage: Some(preimage),
2818 })
2819 }
2820
2821 pub async fn get_registered_incoming_contract_and_client_v2(
2825 &self,
2826 payment_image: PaymentImage,
2827 amount_msats: u64,
2828 ) -> Result<(IncomingContract, ClientHandleArc)> {
2829 let registered_incoming_contract = self
2830 .gateway_db
2831 .begin_transaction_nc()
2832 .await
2833 .load_registered_incoming_contract(payment_image)
2834 .await
2835 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2836 "No corresponding decryption contract available".to_string(),
2837 )))?;
2838
2839 if registered_incoming_contract.incoming_amount_msats != amount_msats {
2840 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2841 "The available decryption contract's amount is not equal to the requested amount"
2842 .to_string(),
2843 )));
2844 }
2845
2846 let client = self
2847 .select_client(registered_incoming_contract.federation_id)
2848 .await?
2849 .into_value();
2850
2851 Ok((registered_incoming_contract.contract, client))
2852 }
2853}
2854
2855#[async_trait]
2856impl IGatewayClientV2 for Gateway {
2857 async fn complete_htlc(&self, htlc_response: InterceptPaymentResponse) {
2858 loop {
2859 match self.get_lightning_context().await {
2860 Ok(lightning_context) => {
2861 match lightning_context
2862 .lnrpc
2863 .complete_htlc(htlc_response.clone())
2864 .await
2865 {
2866 Ok(..) => return,
2867 Err(err) => {
2868 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2869 }
2870 }
2871 }
2872 Err(err) => {
2873 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2874 }
2875 }
2876
2877 sleep(Duration::from_secs(5)).await;
2878 }
2879 }
2880
2881 async fn is_direct_swap(
2882 &self,
2883 invoice: &Bolt11Invoice,
2884 ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>> {
2885 let lightning_context = self.get_lightning_context().await?;
2886 if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
2887 let (contract, client) = self
2888 .get_registered_incoming_contract_and_client_v2(
2889 PaymentImage::Hash(*invoice.payment_hash()),
2890 invoice
2891 .amount_milli_satoshis()
2892 .expect("The amount invoice has been previously checked"),
2893 )
2894 .await?;
2895 Ok(Some((contract, client)))
2896 } else {
2897 Ok(None)
2898 }
2899 }
2900
2901 async fn pay(
2902 &self,
2903 invoice: Bolt11Invoice,
2904 max_delay: u64,
2905 max_fee: Amount,
2906 ) -> std::result::Result<[u8; 32], LightningRpcError> {
2907 let lightning_context = self.get_lightning_context().await?;
2908 lightning_context
2909 .lnrpc
2910 .pay(invoice, max_delay, max_fee)
2911 .await
2912 .map(|response| response.preimage.0)
2913 }
2914
2915 async fn min_contract_amount(
2916 &self,
2917 federation_id: &FederationId,
2918 amount: u64,
2919 ) -> anyhow::Result<Amount> {
2920 Ok(self
2921 .routing_info_v2(federation_id)
2922 .await?
2923 .ok_or(anyhow!("Routing Info not available"))?
2924 .send_fee_minimum
2925 .add_to(amount))
2926 }
2927
2928 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
2929 let rhints = invoice.route_hints();
2930 match rhints.first().and_then(|rh| rh.0.last()) {
2931 None => None,
2932 Some(hop) => match self.get_lightning_context().await {
2933 Ok(lightning_context) => {
2934 if hop.src_node_id != lightning_context.lightning_public_key {
2935 return None;
2936 }
2937
2938 self.federation_manager
2939 .read()
2940 .await
2941 .get_client_for_index(hop.short_channel_id)
2942 }
2943 Err(_) => None,
2944 },
2945 }
2946 }
2947
2948 async fn relay_lnv1_swap(
2949 &self,
2950 client: &ClientHandleArc,
2951 invoice: &Bolt11Invoice,
2952 ) -> anyhow::Result<FinalReceiveState> {
2953 let swap_params = SwapParameters {
2954 payment_hash: *invoice.payment_hash(),
2955 amount_msat: Amount::from_msats(
2956 invoice
2957 .amount_milli_satoshis()
2958 .ok_or(anyhow!("Amountless invoice not supported"))?,
2959 ),
2960 };
2961 let lnv1 = client
2962 .get_first_module::<GatewayClientModule>()
2963 .expect("No LNv1 module");
2964 let operation_id = lnv1.gateway_handle_direct_swap(swap_params).await?;
2965 let mut stream = lnv1
2966 .gateway_subscribe_ln_receive(operation_id)
2967 .await?
2968 .into_stream();
2969 let mut final_state = FinalReceiveState::Failure;
2970 while let Some(update) = stream.next().await {
2971 match update {
2972 GatewayExtReceiveStates::Funding => {}
2973 GatewayExtReceiveStates::FundingFailed { error: _ } => {
2974 final_state = FinalReceiveState::Rejected;
2975 }
2976 GatewayExtReceiveStates::Preimage(preimage) => {
2977 final_state = FinalReceiveState::Success(preimage.0);
2978 }
2979 GatewayExtReceiveStates::RefundError {
2980 error_message: _,
2981 error: _,
2982 } => {
2983 final_state = FinalReceiveState::Failure;
2984 }
2985 GatewayExtReceiveStates::RefundSuccess {
2986 out_points: _,
2987 error: _,
2988 } => {
2989 final_state = FinalReceiveState::Refunded;
2990 }
2991 }
2992 }
2993
2994 Ok(final_state)
2995 }
2996}
2997
2998#[async_trait]
2999impl IGatewayClientV1 for Gateway {
3000 async fn verify_preimage_authentication(
3001 &self,
3002 payment_hash: sha256::Hash,
3003 preimage_auth: sha256::Hash,
3004 contract: OutgoingContractAccount,
3005 ) -> std::result::Result<(), OutgoingPaymentError> {
3006 let mut dbtx = self.gateway_db.begin_transaction().await;
3007 if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
3008 if secret_hash != preimage_auth {
3009 return Err(OutgoingPaymentError {
3010 error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
3011 contract_id: contract.contract.contract_id(),
3012 contract: Some(contract),
3013 });
3014 }
3015 } else {
3016 dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
3019 .await;
3020 return dbtx
3021 .commit_tx_result()
3022 .await
3023 .map_err(|_| OutgoingPaymentError {
3024 error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
3025 contract_id: contract.contract.contract_id(),
3026 contract: Some(contract),
3027 });
3028 }
3029
3030 Ok(())
3031 }
3032
3033 async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()> {
3034 let lightning_context = self.get_lightning_context().await?;
3035
3036 if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
3037 ensure!(
3038 lightning_context.lnrpc.supports_private_payments(),
3039 "Private payments are not supported by the lightning node"
3040 );
3041 }
3042
3043 Ok(())
3044 }
3045
3046 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
3047 let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
3048 gateway_dbtx
3049 .load_federation_config(federation_id)
3050 .await
3051 .map(|c| c.lightning_fee.into())
3052 }
3053
3054 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
3055 self.federation_manager
3056 .read()
3057 .await
3058 .client(federation_id)
3059 .cloned()
3060 }
3061
3062 async fn get_client_for_invoice(
3063 &self,
3064 payment_data: PaymentData,
3065 ) -> Option<Spanned<ClientHandleArc>> {
3066 let rhints = payment_data.route_hints();
3067 match rhints.first().and_then(|rh| rh.0.last()) {
3068 None => None,
3069 Some(hop) => match self.get_lightning_context().await {
3070 Ok(lightning_context) => {
3071 if hop.src_node_id != lightning_context.lightning_public_key {
3072 return None;
3073 }
3074
3075 self.federation_manager
3076 .read()
3077 .await
3078 .get_client_for_index(hop.short_channel_id)
3079 }
3080 Err(_) => None,
3081 },
3082 }
3083 }
3084
3085 async fn pay(
3086 &self,
3087 payment_data: PaymentData,
3088 max_delay: u64,
3089 max_fee: Amount,
3090 ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
3091 let lightning_context = self.get_lightning_context().await?;
3092
3093 match payment_data {
3094 PaymentData::Invoice(invoice) => {
3095 lightning_context
3096 .lnrpc
3097 .pay(invoice, max_delay, max_fee)
3098 .await
3099 }
3100 PaymentData::PrunedInvoice(invoice) => {
3101 lightning_context
3102 .lnrpc
3103 .pay_private(invoice, max_delay, max_fee)
3104 .await
3105 }
3106 }
3107 }
3108
3109 async fn complete_htlc(
3110 &self,
3111 htlc: InterceptPaymentResponse,
3112 ) -> std::result::Result<(), LightningRpcError> {
3113 let lightning_context = loop {
3115 match self.get_lightning_context().await {
3116 Ok(lightning_context) => break lightning_context,
3117 Err(err) => {
3118 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
3119 sleep(Duration::from_secs(5)).await;
3120 }
3121 }
3122 };
3123
3124 lightning_context.lnrpc.complete_htlc(htlc).await
3125 }
3126
3127 async fn is_lnv2_direct_swap(
3128 &self,
3129 payment_hash: sha256::Hash,
3130 amount: Amount,
3131 ) -> anyhow::Result<
3132 Option<(
3133 fedimint_lnv2_common::contracts::IncomingContract,
3134 ClientHandleArc,
3135 )>,
3136 > {
3137 let (contract, client) = self
3138 .get_registered_incoming_contract_and_client_v2(
3139 PaymentImage::Hash(payment_hash),
3140 amount.msats,
3141 )
3142 .await?;
3143 Ok(Some((contract, client)))
3144 }
3145}