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