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