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_api_client::api::net::ConnectorType;
48use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic};
49use fedimint_bitcoind::bitcoincore::BitcoindClient;
50use fedimint_bitcoind::{BlockchainInfo, EsploraClient, IBitcoindRpc};
51use fedimint_client::module_init::ClientModuleInitRegistry;
52use fedimint_client::secret::RootSecretStrategy;
53use fedimint_client::{Client, ClientHandleArc};
54use fedimint_core::config::FederationId;
55use fedimint_core::core::OperationId;
56use fedimint_core::db::{Committable, Database, DatabaseTransaction, apply_migrations};
57use fedimint_core::envs::is_env_var_set;
58use fedimint_core::invite_code::InviteCode;
59use fedimint_core::module::CommonModuleInit;
60use fedimint_core::module::registry::ModuleDecoderRegistry;
61use fedimint_core::rustls::install_crypto_provider;
62use fedimint_core::secp256k1::PublicKey;
63use fedimint_core::secp256k1::schnorr::Signature;
64use fedimint_core::task::{TaskGroup, TaskHandle, TaskShutdownToken, sleep};
65use fedimint_core::time::duration_since_epoch;
66use fedimint_core::util::{FmtCompact, FmtCompactAnyhow, SafeUrl, Spanned};
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, CreateInvoiceForOperatorPayload, CreateOfferPayload, CreateOfferResponse,
74 DepositAddressPayload, DepositAddressRecheckPayload, FederationBalanceInfo, FederationConfig,
75 FederationInfo, GatewayBalances, GatewayFedConfig, GatewayInfo, GetInvoiceRequest,
76 GetInvoiceResponse, LeaveFedPayload, LightningInfo, LightningMode, ListTransactionsPayload,
77 ListTransactionsResponse, MnemonicResponse, OpenChannelRequest, PayInvoiceForOperatorPayload,
78 PayOfferPayload, PayOfferResponse, PaymentLogPayload, PaymentLogResponse, PaymentStats,
79 PaymentSummaryPayload, PaymentSummaryResponse, ReceiveEcashPayload, ReceiveEcashResponse,
80 RegisteredProtocol, SendOnchainRequest, SetFeesPayload, SpendEcashPayload, SpendEcashResponse,
81 V1_API_ENDPOINT, WithdrawPayload, WithdrawPreviewPayload, WithdrawPreviewResponse,
82 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());
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_payment_log_msg(
1199 &self,
1200 PaymentLogPayload {
1201 end_position,
1202 pagination_size,
1203 federation_id,
1204 event_kinds,
1205 }: PaymentLogPayload,
1206 ) -> AdminResult<PaymentLogResponse> {
1207 const BATCH_SIZE: u64 = 10_000;
1208 let federation_manager = self.federation_manager.read().await;
1209 let client = federation_manager
1210 .client(&federation_id)
1211 .ok_or(FederationNotConnected {
1212 federation_id_prefix: federation_id.to_prefix(),
1213 })?
1214 .value();
1215
1216 let event_kinds = if event_kinds.is_empty() {
1217 ALL_GATEWAY_EVENTS.to_vec()
1218 } else {
1219 event_kinds
1220 };
1221
1222 let end_position = if let Some(position) = end_position {
1223 position
1224 } else {
1225 let mut dbtx = client.db().begin_transaction_nc().await;
1226 dbtx.get_next_event_log_id().await
1227 };
1228
1229 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
1230
1231 let mut payment_log = Vec::new();
1232
1233 while payment_log.len() < pagination_size {
1234 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
1235 let mut filtered_batch = batch
1236 .into_iter()
1237 .filter(|e| e.id() <= end_position && event_kinds.contains(&e.as_raw().kind))
1238 .collect::<Vec<_>>();
1239 filtered_batch.reverse();
1240 payment_log.extend(filtered_batch);
1241
1242 start_position = start_position.saturating_sub(BATCH_SIZE);
1244
1245 if start_position == EventLogId::LOG_START {
1246 break;
1247 }
1248 }
1249
1250 payment_log.truncate(pagination_size);
1252
1253 Ok(PaymentLogResponse(payment_log))
1254 }
1255
1256 pub async fn handle_get_invoice_msg(
1259 &self,
1260 payload: GetInvoiceRequest,
1261 ) -> AdminResult<Option<GetInvoiceResponse>> {
1262 let lightning_context = self.get_lightning_context().await?;
1263 let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1264 Ok(invoice)
1265 }
1266
1267 pub async fn handle_create_offer_for_operator_msg(
1269 &self,
1270 payload: CreateOfferPayload,
1271 ) -> AdminResult<CreateOfferResponse> {
1272 let lightning_context = self.get_lightning_context().await?;
1273 let offer = lightning_context.lnrpc.create_offer(
1274 payload.amount,
1275 payload.description,
1276 payload.expiry_secs,
1277 payload.quantity,
1278 )?;
1279 Ok(CreateOfferResponse { offer })
1280 }
1281
1282 pub async fn handle_pay_offer_for_operator_msg(
1284 &self,
1285 payload: PayOfferPayload,
1286 ) -> AdminResult<PayOfferResponse> {
1287 let lightning_context = self.get_lightning_context().await?;
1288 let preimage = lightning_context
1289 .lnrpc
1290 .pay_offer(
1291 payload.offer,
1292 payload.quantity,
1293 payload.amount,
1294 payload.payer_note,
1295 )
1296 .await?;
1297 Ok(PayOfferResponse {
1298 preimage: preimage.to_string(),
1299 })
1300 }
1301
1302 async fn register_federations(
1304 &self,
1305 federations: &BTreeMap<FederationId, FederationConfig>,
1306 register_task_group: &TaskGroup,
1307 ) {
1308 if let Ok(lightning_context) = self.get_lightning_context().await {
1309 let route_hints = lightning_context
1310 .lnrpc
1311 .parsed_route_hints(self.num_route_hints)
1312 .await;
1313 if route_hints.is_empty() {
1314 warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
1315 }
1316
1317 for (federation_id, federation_config) in federations {
1318 let fed_manager = self.federation_manager.read().await;
1319 if let Some(client) = fed_manager.client(federation_id) {
1320 let client_arc = client.clone().into_value();
1321 let route_hints = route_hints.clone();
1322 let lightning_context = lightning_context.clone();
1323 let federation_config = federation_config.clone();
1324 let registrations =
1325 self.registrations.clone().into_values().collect::<Vec<_>>();
1326
1327 register_task_group.spawn_cancellable_silent(
1328 "register federation",
1329 async move {
1330 let Ok(gateway_client) =
1331 client_arc.get_first_module::<GatewayClientModule>()
1332 else {
1333 return;
1334 };
1335
1336 for registration in registrations {
1337 gateway_client
1338 .try_register_with_federation(
1339 route_hints.clone(),
1340 GW_ANNOUNCEMENT_TTL,
1341 federation_config.lightning_fee.into(),
1342 lightning_context.clone(),
1343 registration.endpoint_url,
1344 registration.keypair.public_key(),
1345 )
1346 .await;
1347 }
1348 },
1349 );
1350 }
1351 }
1352 }
1353 }
1354
1355 pub async fn select_client(
1358 &self,
1359 federation_id: FederationId,
1360 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
1361 {
1362 self.federation_manager
1363 .read()
1364 .await
1365 .client(&federation_id)
1366 .cloned()
1367 .ok_or(FederationNotConnected {
1368 federation_id_prefix: federation_id.to_prefix(),
1369 })
1370 }
1371
1372 async fn load_or_generate_mnemonic(gateway_db: &Database) -> AdminResult<Mnemonic> {
1377 Ok(
1378 if let Ok(entropy) = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db).await {
1379 Mnemonic::from_entropy(&entropy)
1380 .map_err(|e| AdminGatewayError::MnemonicError(anyhow!(e.to_string())))?
1381 } else {
1382 let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
1383 info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
1384 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
1385 |e| {
1386 AdminGatewayError::MnemonicError(anyhow!(format!(
1387 "Seed phrase provided in environment was invalid {e:?}"
1388 )))
1389 },
1390 )?
1391 } else {
1392 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
1393 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
1394 };
1395
1396 Client::store_encodable_client_secret(gateway_db, mnemonic.to_entropy())
1397 .await
1398 .map_err(AdminGatewayError::MnemonicError)?;
1399 mnemonic
1400 },
1401 )
1402 }
1403
1404 async fn load_clients(&self) -> AdminResult<()> {
1408 let mut federation_manager = self.federation_manager.write().await;
1409
1410 let configs = {
1411 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1412 dbtx.load_federation_configs().await
1413 };
1414
1415 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
1416 federation_manager.set_next_index(max_federation_index + 1);
1417 }
1418
1419 for (federation_id, config) in configs {
1420 let federation_index = config.federation_index;
1421 match Box::pin(Spanned::try_new(
1422 info_span!(target: LOG_GATEWAY, "client", federation_id = %federation_id.clone()),
1423 self.client_builder
1424 .build(config, Arc::new(self.clone()), &self.mnemonic),
1425 ))
1426 .await
1427 {
1428 Ok(client) => {
1429 federation_manager.add_client(federation_index, client);
1430 }
1431 _ => {
1432 warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
1433 }
1434 }
1435 }
1436
1437 Ok(())
1438 }
1439
1440 fn register_clients_timer(&self) {
1446 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1448 info!(target: LOG_GATEWAY, "Spawning register task...");
1449 let gateway = self.clone();
1450 let register_task_group = self.task_group.make_subgroup();
1451 self.task_group.spawn_cancellable("register clients", async move {
1452 loop {
1453 let gateway_state = gateway.get_state().await;
1454 if let GatewayState::Running { .. } = &gateway_state {
1455 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
1456 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
1457 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
1458 } else {
1459 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
1461 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");
1462 sleep(NOT_RUNNING_RETRY).await;
1463 continue;
1464 }
1465
1466 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
1469 }
1470 });
1471 }
1472 }
1473
1474 async fn check_federation_network(
1477 client: &ClientHandleArc,
1478 network: Network,
1479 ) -> AdminResult<()> {
1480 let federation_id = client.federation_id();
1481 let config = client.config().await;
1482
1483 let lnv1_cfg = config
1484 .modules
1485 .values()
1486 .find(|m| LightningCommonInit::KIND == m.kind);
1487
1488 let lnv2_cfg = config
1489 .modules
1490 .values()
1491 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
1492
1493 if lnv1_cfg.is_none() && lnv2_cfg.is_none() {
1495 return Err(AdminGatewayError::ClientCreationError(anyhow!(
1496 "Federation {federation_id} does not have any lightning module (LNv1 or LNv2)"
1497 )));
1498 }
1499
1500 if let Some(cfg) = lnv1_cfg {
1502 let ln_cfg: &LightningClientConfig = cfg.cast()?;
1503
1504 if ln_cfg.network.0 != network {
1505 crit!(
1506 target: LOG_GATEWAY,
1507 federation_id = %federation_id,
1508 network = %network,
1509 "Incorrect LNv1 network for federation",
1510 );
1511 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1512 "Unsupported LNv1 network {}",
1513 ln_cfg.network
1514 ))));
1515 }
1516 }
1517
1518 if let Some(cfg) = lnv2_cfg {
1520 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig = cfg.cast()?;
1521
1522 if ln_cfg.network != network {
1523 crit!(
1524 target: LOG_GATEWAY,
1525 federation_id = %federation_id,
1526 network = %network,
1527 "Incorrect LNv2 network for federation",
1528 );
1529 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
1530 "Unsupported LNv2 network {}",
1531 ln_cfg.network
1532 ))));
1533 }
1534 }
1535
1536 Ok(())
1537 }
1538
1539 pub async fn get_lightning_context(
1543 &self,
1544 ) -> std::result::Result<LightningContext, LightningRpcError> {
1545 match self.get_state().await {
1546 GatewayState::Running { lightning_context }
1547 | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
1548 _ => Err(LightningRpcError::FailedToConnect),
1549 }
1550 }
1551
1552 pub async fn unannounce_from_all_federations(&self) {
1555 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1556 for registration in self.registrations.values() {
1557 self.federation_manager
1558 .read()
1559 .await
1560 .unannounce_from_all_federations(registration.keypair)
1561 .await;
1562 }
1563 }
1564 }
1565
1566 fn create_lightning_client(
1567 &self,
1568 runtime: Arc<tokio::runtime::Runtime>,
1569 ) -> Box<dyn ILnRpcClient> {
1570 match self.lightning_mode.clone() {
1571 LightningMode::Lnd {
1572 lnd_rpc_addr,
1573 lnd_tls_cert,
1574 lnd_macaroon,
1575 } => Box::new(GatewayLndClient::new(
1576 lnd_rpc_addr,
1577 lnd_tls_cert,
1578 lnd_macaroon,
1579 None,
1580 )),
1581 LightningMode::Ldk {
1582 lightning_port,
1583 alias,
1584 } => Box::new(
1585 ldk::GatewayLdkClient::new(
1586 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
1587 self.chain_source.clone(),
1588 self.network,
1589 lightning_port,
1590 alias,
1591 self.mnemonic.clone(),
1592 runtime,
1593 )
1594 .expect("Failed to create LDK client"),
1595 ),
1596 }
1597 }
1598}
1599
1600#[async_trait]
1601impl IAdminGateway for Gateway {
1602 type Error = AdminGatewayError;
1603
1604 async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
1607 let GatewayState::Running { lightning_context } = self.get_state().await else {
1608 return Ok(GatewayInfo {
1609 federations: vec![],
1610 federation_fake_scids: None,
1611 version_hash: fedimint_build_code_version_env!().to_string(),
1612 gateway_state: self.state.read().await.to_string(),
1613 lightning_info: LightningInfo::NotConnected,
1614 lightning_mode: self.lightning_mode.clone(),
1615 registrations: self
1616 .registrations
1617 .iter()
1618 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
1619 .collect(),
1620 });
1621 };
1622
1623 let dbtx = self.gateway_db.begin_transaction_nc().await;
1624 let federations = self
1625 .federation_manager
1626 .read()
1627 .await
1628 .federation_info_all_federations(dbtx)
1629 .await;
1630
1631 let channels: BTreeMap<u64, FederationId> = federations
1632 .iter()
1633 .map(|federation_info| {
1634 (
1635 federation_info.config.federation_index,
1636 federation_info.federation_id,
1637 )
1638 })
1639 .collect();
1640
1641 let lightning_info = lightning_context.lnrpc.parsed_node_info().await;
1642
1643 Ok(GatewayInfo {
1644 federations,
1645 federation_fake_scids: Some(channels),
1646 version_hash: fedimint_build_code_version_env!().to_string(),
1647 gateway_state: self.state.read().await.to_string(),
1648 lightning_info,
1649 lightning_mode: self.lightning_mode.clone(),
1650 registrations: self
1651 .registrations
1652 .iter()
1653 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
1654 .collect(),
1655 })
1656 }
1657
1658 async fn handle_list_channels_msg(
1661 &self,
1662 ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
1663 let context = self.get_lightning_context().await?;
1664 let response = context.lnrpc.list_channels().await?;
1665 Ok(response.channels)
1666 }
1667
1668 async fn handle_payment_summary_msg(
1671 &self,
1672 PaymentSummaryPayload {
1673 start_millis,
1674 end_millis,
1675 }: PaymentSummaryPayload,
1676 ) -> AdminResult<PaymentSummaryResponse> {
1677 let federation_manager = self.federation_manager.read().await;
1678 let fed_configs = federation_manager.get_all_federation_configs().await;
1679 let federation_ids = fed_configs.keys().collect::<Vec<_>>();
1680 let start = UNIX_EPOCH + Duration::from_millis(start_millis);
1681 let end = UNIX_EPOCH + Duration::from_millis(end_millis);
1682
1683 if start > end {
1684 return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
1685 }
1686
1687 let mut outgoing = StructuredPaymentEvents::default();
1688 let mut incoming = StructuredPaymentEvents::default();
1689 for fed_id in federation_ids {
1690 let client = federation_manager
1691 .client(fed_id)
1692 .expect("No client available")
1693 .value();
1694 let all_events = &get_events_for_duration(client, start, end).await;
1695
1696 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
1697 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
1698 outgoing.combine(&mut lnv1_outgoing);
1699 incoming.combine(&mut lnv1_incoming);
1700 outgoing.combine(&mut lnv2_outgoing);
1701 incoming.combine(&mut lnv2_incoming);
1702 }
1703
1704 Ok(PaymentSummaryResponse {
1705 outgoing: PaymentStats::compute(&outgoing),
1706 incoming: PaymentStats::compute(&incoming),
1707 })
1708 }
1709
1710 async fn handle_leave_federation(
1715 &self,
1716 payload: LeaveFedPayload,
1717 ) -> AdminResult<FederationInfo> {
1718 let mut federation_manager = self.federation_manager.write().await;
1721 let mut dbtx = self.gateway_db.begin_transaction().await;
1722
1723 let federation_info = federation_manager
1724 .leave_federation(
1725 payload.federation_id,
1726 &mut dbtx.to_ref_nc(),
1727 self.registrations.values().collect(),
1728 )
1729 .await?;
1730
1731 dbtx.remove_federation_config(payload.federation_id).await;
1732 dbtx.commit_tx().await;
1733 Ok(federation_info)
1734 }
1735
1736 async fn handle_connect_federation(
1741 &self,
1742 payload: ConnectFedPayload,
1743 ) -> AdminResult<FederationInfo> {
1744 let GatewayState::Running { lightning_context } = self.get_state().await else {
1745 return Err(AdminGatewayError::Lightning(
1746 LightningRpcError::FailedToConnect,
1747 ));
1748 };
1749
1750 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
1751 AdminGatewayError::ClientCreationError(anyhow!(format!(
1752 "Invalid federation member string {e:?}"
1753 )))
1754 })?;
1755
1756 #[cfg(feature = "tor")]
1757 let connector = match &payload.use_tor {
1758 Some(true) => ConnectorType::tor(),
1759 Some(false) => ConnectorType::default(),
1760 None => {
1761 debug!(target: LOG_GATEWAY, "Missing `use_tor` payload field, defaulting to `Connector::Tcp` variant!");
1762 ConnectorType::default()
1763 }
1764 };
1765
1766 #[cfg(not(feature = "tor"))]
1767 let connector = ConnectorType::default();
1768
1769 let federation_id = invite_code.federation_id();
1770
1771 let mut federation_manager = self.federation_manager.write().await;
1772
1773 if federation_manager.has_federation(federation_id) {
1775 return Err(AdminGatewayError::ClientCreationError(anyhow!(
1776 "Federation has already been registered"
1777 )));
1778 }
1779
1780 let federation_index = federation_manager.pop_next_index()?;
1783
1784 let federation_config = FederationConfig {
1785 invite_code,
1786 federation_index,
1787 lightning_fee: self.default_routing_fees,
1788 transaction_fee: self.default_transaction_fees,
1789 connector,
1790 };
1791
1792 let recover = payload.recover.unwrap_or(false);
1793 if recover {
1794 self.client_builder
1795 .recover(
1796 federation_config.clone(),
1797 Arc::new(self.clone()),
1798 &self.mnemonic,
1799 )
1800 .await?;
1801 }
1802
1803 let client = self
1804 .client_builder
1805 .build(
1806 federation_config.clone(),
1807 Arc::new(self.clone()),
1808 &self.mnemonic,
1809 )
1810 .await?;
1811
1812 if recover {
1813 client.wait_for_all_active_state_machines().await?;
1814 }
1815
1816 let federation_info = FederationInfo {
1819 federation_id,
1820 federation_name: federation_manager.federation_name(&client).await,
1821 balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
1822 warn!(
1823 target: LOG_GATEWAY,
1824 err = %err.fmt_compact_anyhow(),
1825 %federation_id,
1826 "Balance not immediately available after joining/recovering."
1827 );
1828 Amount::default()
1829 }),
1830 config: federation_config.clone(),
1831 last_backup_time: None,
1832 };
1833
1834 Self::check_federation_network(&client, self.network).await?;
1835 if matches!(self.lightning_mode, LightningMode::Lnd { .. })
1836 && let Ok(lnv1) = client.get_first_module::<GatewayClientModule>()
1837 {
1838 for registration in self.registrations.values() {
1839 lnv1.try_register_with_federation(
1840 Vec::new(),
1842 GW_ANNOUNCEMENT_TTL,
1843 federation_config.lightning_fee.into(),
1844 lightning_context.clone(),
1845 registration.endpoint_url.clone(),
1846 registration.keypair.public_key(),
1847 )
1848 .await;
1849 }
1850 }
1851
1852 federation_manager.add_client(
1854 federation_index,
1855 Spanned::new(
1856 info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
1857 async { client },
1858 )
1859 .await,
1860 );
1861
1862 let mut dbtx = self.gateway_db.begin_transaction().await;
1863 dbtx.save_federation_config(&federation_config).await;
1864 dbtx.save_federation_backup_record(federation_id, None)
1865 .await;
1866 dbtx.commit_tx().await;
1867 debug!(
1868 target: LOG_GATEWAY,
1869 federation_id = %federation_id,
1870 federation_index = %federation_index,
1871 "Federation connected"
1872 );
1873
1874 Ok(federation_info)
1875 }
1876
1877 async fn handle_set_fees_msg(
1880 &self,
1881 SetFeesPayload {
1882 federation_id,
1883 lightning_base,
1884 lightning_parts_per_million,
1885 transaction_base,
1886 transaction_parts_per_million,
1887 }: SetFeesPayload,
1888 ) -> AdminResult<()> {
1889 let mut dbtx = self.gateway_db.begin_transaction().await;
1890 let mut fed_configs = if let Some(fed_id) = federation_id {
1891 dbtx.load_federation_configs()
1892 .await
1893 .into_iter()
1894 .filter(|(id, _)| *id == fed_id)
1895 .collect::<BTreeMap<_, _>>()
1896 } else {
1897 dbtx.load_federation_configs().await
1898 };
1899
1900 let federation_manager = self.federation_manager.read().await;
1901
1902 for (federation_id, config) in &mut fed_configs {
1903 let mut lightning_fee = config.lightning_fee;
1904 if let Some(lightning_base) = lightning_base {
1905 lightning_fee.base = lightning_base;
1906 }
1907
1908 if let Some(lightning_ppm) = lightning_parts_per_million {
1909 lightning_fee.parts_per_million = lightning_ppm;
1910 }
1911
1912 let mut transaction_fee = config.transaction_fee;
1913 if let Some(transaction_base) = transaction_base {
1914 transaction_fee.base = transaction_base;
1915 }
1916
1917 if let Some(transaction_ppm) = transaction_parts_per_million {
1918 transaction_fee.parts_per_million = transaction_ppm;
1919 }
1920
1921 let client =
1922 federation_manager
1923 .client(federation_id)
1924 .ok_or(FederationNotConnected {
1925 federation_id_prefix: federation_id.to_prefix(),
1926 })?;
1927 let client_config = client.value().config().await;
1928 let contains_lnv2 = client_config
1929 .modules
1930 .values()
1931 .any(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
1932
1933 let send_fees = lightning_fee + transaction_fee;
1935 if contains_lnv2 && send_fees.gt(&PaymentFee::SEND_FEE_LIMIT) {
1936 return Err(AdminGatewayError::GatewayConfigurationError(format!(
1937 "Total Send fees exceeded {}",
1938 PaymentFee::SEND_FEE_LIMIT
1939 )));
1940 }
1941
1942 if contains_lnv2 && transaction_fee.gt(&PaymentFee::RECEIVE_FEE_LIMIT) {
1944 return Err(AdminGatewayError::GatewayConfigurationError(format!(
1945 "Transaction fees exceeded RECEIVE LIMIT {}",
1946 PaymentFee::RECEIVE_FEE_LIMIT
1947 )));
1948 }
1949
1950 config.lightning_fee = lightning_fee;
1951 config.transaction_fee = transaction_fee;
1952 dbtx.save_federation_config(config).await;
1953 }
1954
1955 dbtx.commit_tx().await;
1956
1957 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1958 let register_task_group = TaskGroup::new();
1959
1960 self.register_federations(&fed_configs, ®ister_task_group)
1961 .await;
1962 }
1963
1964 Ok(())
1965 }
1966
1967 async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
1971 let words = self
1972 .mnemonic
1973 .words()
1974 .map(std::string::ToString::to_string)
1975 .collect::<Vec<_>>();
1976 let all_federations = self
1977 .federation_manager
1978 .read()
1979 .await
1980 .get_all_federation_configs()
1981 .await
1982 .keys()
1983 .copied()
1984 .collect::<BTreeSet<_>>();
1985 let legacy_federations = self.client_builder.legacy_federations(all_federations);
1986 let mnemonic_response = MnemonicResponse {
1987 mnemonic: words,
1988 legacy_federations,
1989 };
1990 Ok(mnemonic_response)
1991 }
1992
1993 async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
1996 info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
1997 let context = self.get_lightning_context().await?;
1998 let res = context.lnrpc.open_channel(payload).await?;
1999 info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
2000 Txid::from_str(&res.funding_txid).map_err(|e| {
2001 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2002 failure_reason: format!("Received invalid channel funding txid string {e}"),
2003 })
2004 })
2005 }
2006
2007 async fn handle_close_channels_with_peer_msg(
2010 &self,
2011 payload: CloseChannelsWithPeerRequest,
2012 ) -> AdminResult<CloseChannelsWithPeerResponse> {
2013 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Closing lightning channel...");
2014 let context = self.get_lightning_context().await?;
2015 let response = context
2016 .lnrpc
2017 .close_channels_with_peer(payload.clone())
2018 .await?;
2019 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Initiated channel closure");
2020 Ok(response)
2021 }
2022
2023 async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
2026 let dbtx = self.gateway_db.begin_transaction_nc().await;
2027 let federation_infos = self
2028 .federation_manager
2029 .read()
2030 .await
2031 .federation_info_all_federations(dbtx)
2032 .await;
2033
2034 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
2035 .iter()
2036 .map(|federation_info| FederationBalanceInfo {
2037 federation_id: federation_info.federation_id,
2038 ecash_balance_msats: Amount {
2039 msats: federation_info.balance_msat.msats,
2040 },
2041 })
2042 .collect();
2043
2044 let context = self.get_lightning_context().await?;
2045 let lightning_node_balances = context.lnrpc.get_balances().await?;
2046
2047 Ok(GatewayBalances {
2048 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
2049 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
2050 ecash_balances,
2051 inbound_lightning_liquidity_msats: lightning_node_balances
2052 .inbound_lightning_liquidity_msats,
2053 })
2054 }
2055
2056 async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
2058 let context = self.get_lightning_context().await?;
2059 let response = context.lnrpc.send_onchain(payload.clone()).await?;
2060 let txid =
2061 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
2062 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
2063 })?;
2064 info!(onchain_request = %payload, txid = %txid, "Sent onchain transaction");
2065 Ok(txid)
2066 }
2067
2068 async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
2070 let context = self.get_lightning_context().await?;
2071 let response = context.lnrpc.get_ln_onchain_address().await?;
2072
2073 let address = Address::from_str(&response.address).map_err(|e| {
2074 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2075 failure_reason: e.to_string(),
2076 })
2077 })?;
2078
2079 address.require_network(self.network).map_err(|e| {
2080 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2081 failure_reason: e.to_string(),
2082 })
2083 })
2084 }
2085
2086 async fn handle_deposit_address_msg(
2087 &self,
2088 payload: DepositAddressPayload,
2089 ) -> AdminResult<Address> {
2090 self.handle_address_msg(payload).await
2091 }
2092
2093 async fn handle_receive_ecash_msg(
2094 &self,
2095 payload: ReceiveEcashPayload,
2096 ) -> AdminResult<ReceiveEcashResponse> {
2097 Self::handle_receive_ecash_msg(self, payload)
2098 .await
2099 .map_err(|e| AdminGatewayError::Unexpected(anyhow::anyhow!("{}", e)))
2100 }
2101
2102 async fn handle_create_invoice_for_operator_msg(
2105 &self,
2106 payload: CreateInvoiceForOperatorPayload,
2107 ) -> AdminResult<Bolt11Invoice> {
2108 let GatewayState::Running { lightning_context } = self.get_state().await else {
2109 return Err(AdminGatewayError::Lightning(
2110 LightningRpcError::FailedToConnect,
2111 ));
2112 };
2113
2114 Bolt11Invoice::from_str(
2115 &lightning_context
2116 .lnrpc
2117 .create_invoice(CreateInvoiceRequest {
2118 payment_hash: None, amount_msat: payload.amount_msats,
2121 expiry_secs: payload.expiry_secs.unwrap_or(3600),
2122 description: payload.description.map(InvoiceDescription::Direct),
2123 })
2124 .await?
2125 .invoice,
2126 )
2127 .map_err(|e| {
2128 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2129 failure_reason: e.to_string(),
2130 })
2131 })
2132 }
2133
2134 async fn handle_pay_invoice_for_operator_msg(
2137 &self,
2138 payload: PayInvoiceForOperatorPayload,
2139 ) -> AdminResult<Preimage> {
2140 const BASE_FEE: u64 = 50;
2142 const FEE_DENOMINATOR: u64 = 100;
2143 const MAX_DELAY: u64 = 1008;
2144
2145 let GatewayState::Running { lightning_context } = self.get_state().await else {
2146 return Err(AdminGatewayError::Lightning(
2147 LightningRpcError::FailedToConnect,
2148 ));
2149 };
2150
2151 let max_fee = BASE_FEE
2152 + payload
2153 .invoice
2154 .amount_milli_satoshis()
2155 .context("Invoice is missing amount")?
2156 .saturating_div(FEE_DENOMINATOR);
2157
2158 let res = lightning_context
2159 .lnrpc
2160 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
2161 .await?;
2162 Ok(res.preimage)
2163 }
2164
2165 async fn handle_list_transactions_msg(
2167 &self,
2168 payload: ListTransactionsPayload,
2169 ) -> AdminResult<ListTransactionsResponse> {
2170 let lightning_context = self.get_lightning_context().await?;
2171 let response = lightning_context
2172 .lnrpc
2173 .list_transactions(payload.start_secs, payload.end_secs)
2174 .await?;
2175 Ok(response)
2176 }
2177
2178 async fn handle_spend_ecash_msg(
2180 &self,
2181 payload: SpendEcashPayload,
2182 ) -> AdminResult<SpendEcashResponse> {
2183 let client = self
2184 .select_client(payload.federation_id)
2185 .await?
2186 .into_value();
2187 let mint_module = client.get_first_module::<MintClientModule>()?;
2188 let timeout = Duration::from_secs(payload.timeout);
2189 let (operation_id, notes) = if payload.allow_overpay {
2190 let (operation_id, notes) = mint_module
2191 .spend_notes_with_selector(
2192 &SelectNotesWithAtleastAmount,
2193 payload.amount,
2194 timeout,
2195 payload.include_invite,
2196 (),
2197 )
2198 .await?;
2199
2200 let overspend_amount = notes.total_amount().saturating_sub(payload.amount);
2201 if overspend_amount != Amount::ZERO {
2202 warn!(
2203 target: LOG_GATEWAY,
2204 overspend_amount = %overspend_amount,
2205 "Selected notes worth more than requested",
2206 );
2207 }
2208
2209 (operation_id, notes)
2210 } else {
2211 mint_module
2212 .spend_notes_with_selector(
2213 &SelectNotesWithExactAmount,
2214 payload.amount,
2215 timeout,
2216 payload.include_invite,
2217 (),
2218 )
2219 .await?
2220 };
2221
2222 debug!(target: LOG_GATEWAY, ?operation_id, ?notes, "Spend ecash notes");
2223
2224 Ok(SpendEcashResponse {
2225 operation_id,
2226 notes,
2227 })
2228 }
2229
2230 async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
2233 let mut state_guard = self.state.write().await;
2235 if let GatewayState::Running { lightning_context } = state_guard.clone() {
2236 *state_guard = GatewayState::ShuttingDown { lightning_context };
2237
2238 self.federation_manager
2239 .read()
2240 .await
2241 .wait_for_incoming_payments()
2242 .await?;
2243 }
2244
2245 let tg = task_group.clone();
2246 tg.spawn("Kill Gateway", |_task_handle| async {
2247 if let Err(err) = task_group.shutdown_join_all(Duration::from_secs(180)).await {
2248 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error shutting down gateway");
2249 }
2250 });
2251 Ok(())
2252 }
2253
2254 fn get_task_group(&self) -> TaskGroup {
2255 self.task_group.clone()
2256 }
2257
2258 async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> AdminResult<WithdrawResponse> {
2261 let WithdrawPayload {
2262 amount,
2263 address,
2264 federation_id,
2265 quoted_fees,
2266 } = payload;
2267
2268 let address_network = get_network_for_address(&address);
2269 let gateway_network = self.network;
2270 let Ok(address) = address.require_network(gateway_network) else {
2271 return Err(AdminGatewayError::WithdrawError {
2272 failure_reason: format!(
2273 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
2274 ),
2275 });
2276 };
2277
2278 let client = self.select_client(federation_id).await?;
2279 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
2280
2281 let (withdraw_amount, fees) = match quoted_fees {
2284 Some(fees) => {
2286 let amt = match amount {
2287 BitcoinAmountOrAll::Amount(a) => a,
2288 BitcoinAmountOrAll::All => {
2289 return Err(AdminGatewayError::WithdrawError {
2291 failure_reason:
2292 "Cannot use 'all' with quoted fees - amount must be resolved first"
2293 .to_string(),
2294 });
2295 }
2296 };
2297 (amt, fees)
2298 }
2299 None => match amount {
2301 BitcoinAmountOrAll::All => {
2304 let balance = bitcoin::Amount::from_sat(
2305 client
2306 .value()
2307 .get_balance_for_btc()
2308 .await
2309 .map_err(|err| {
2310 AdminGatewayError::Unexpected(anyhow!(
2311 "Balance not available: {}",
2312 err.fmt_compact_anyhow()
2313 ))
2314 })?
2315 .msats
2316 / 1000,
2317 );
2318 let fees = wallet_module.get_withdraw_fees(&address, balance).await?;
2319 let withdraw_amount = balance.checked_sub(fees.amount());
2320 if withdraw_amount.is_none() {
2321 return Err(AdminGatewayError::WithdrawError {
2322 failure_reason: format!(
2323 "Insufficient funds. Balance: {balance} Fees: {fees:?}"
2324 ),
2325 });
2326 }
2327 (withdraw_amount.expect("checked above"), fees)
2328 }
2329 BitcoinAmountOrAll::Amount(amount) => (
2330 amount,
2331 wallet_module.get_withdraw_fees(&address, amount).await?,
2332 ),
2333 },
2334 };
2335
2336 let operation_id = wallet_module
2337 .withdraw(&address, withdraw_amount, fees, ())
2338 .await?;
2339 let mut updates = wallet_module
2340 .subscribe_withdraw_updates(operation_id)
2341 .await?
2342 .into_stream();
2343
2344 while let Some(update) = updates.next().await {
2345 match update {
2346 WithdrawState::Succeeded(txid) => {
2347 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds");
2348 return Ok(WithdrawResponse { txid, fees });
2349 }
2350 WithdrawState::Failed(e) => {
2351 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
2352 }
2353 WithdrawState::Created => {}
2354 }
2355 }
2356
2357 Err(AdminGatewayError::WithdrawError {
2358 failure_reason: "Ran out of state updates while withdrawing".to_string(),
2359 })
2360 }
2361
2362 async fn handle_withdraw_preview_msg(
2365 &self,
2366 payload: WithdrawPreviewPayload,
2367 ) -> AdminResult<WithdrawPreviewResponse> {
2368 let gateway_network = self.network;
2369 let address_checked = payload
2370 .address
2371 .clone()
2372 .require_network(gateway_network)
2373 .map_err(|_| AdminGatewayError::WithdrawError {
2374 failure_reason: "Address network mismatch".to_string(),
2375 })?;
2376
2377 let client = self.select_client(payload.federation_id).await?;
2378 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
2379
2380 let WithdrawDetails {
2381 amount,
2382 mint_fees,
2383 peg_out_fees,
2384 } = match payload.amount {
2385 BitcoinAmountOrAll::All => {
2386 calculate_max_withdrawable(client.value(), &address_checked).await?
2387 }
2388 BitcoinAmountOrAll::Amount(btc_amount) => WithdrawDetails {
2389 amount: btc_amount.into(),
2390 mint_fees: None,
2391 peg_out_fees: wallet_module
2392 .get_withdraw_fees(&address_checked, btc_amount)
2393 .await?,
2394 },
2395 };
2396
2397 let total_cost = amount
2398 .checked_add(peg_out_fees.amount().into())
2399 .and_then(|a| a.checked_add(mint_fees.unwrap_or(Amount::ZERO)))
2400 .ok_or_else(|| AdminGatewayError::Unexpected(anyhow!("Total cost overflow")))?;
2401
2402 Ok(WithdrawPreviewResponse {
2403 withdraw_amount: amount,
2404 address: payload.address.assume_checked().to_string(),
2405 peg_out_fees,
2406 total_cost,
2407 mint_fees,
2408 })
2409 }
2410
2411 fn get_password_hash(&self) -> String {
2412 self.bcrypt_password_hash.to_string()
2413 }
2414
2415 fn gatewayd_version(&self) -> String {
2416 let gatewayd_version = env!("CARGO_PKG_VERSION");
2417 gatewayd_version.to_string()
2418 }
2419
2420 async fn get_chain_source(&self) -> (Option<BlockchainInfo>, ChainSource, Network) {
2421 if let Ok(info) = self.bitcoin_rpc.get_info().await {
2422 (Some(info), self.chain_source.clone(), self.network)
2423 } else {
2424 (None, self.chain_source.clone(), self.network)
2425 }
2426 }
2427
2428 fn lightning_mode(&self) -> LightningMode {
2429 self.lightning_mode.clone()
2430 }
2431}
2432
2433impl Gateway {
2435 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
2439 self.federation_manager
2440 .read()
2441 .await
2442 .client(federation_id)
2443 .map(|client| {
2444 client
2445 .value()
2446 .get_first_module::<GatewayClientModuleV2>()
2447 .expect("Must have client module")
2448 .keypair
2449 .public_key()
2450 })
2451 }
2452
2453 pub async fn routing_info_v2(
2456 &self,
2457 federation_id: &FederationId,
2458 ) -> Result<Option<RoutingInfo>> {
2459 let context = self.get_lightning_context().await?;
2460
2461 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2462 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
2463 PublicGatewayError::FederationNotConnected(FederationNotConnected {
2464 federation_id_prefix: federation_id.to_prefix(),
2465 }),
2466 )?;
2467
2468 let lightning_fee = fed_config.lightning_fee;
2469 let transaction_fee = fed_config.transaction_fee;
2470
2471 Ok(self
2472 .public_key_v2(federation_id)
2473 .await
2474 .map(|module_public_key| RoutingInfo {
2475 lightning_public_key: context.lightning_public_key,
2476 module_public_key,
2477 send_fee_default: lightning_fee + transaction_fee,
2478 send_fee_minimum: transaction_fee,
2482 expiration_delta_default: 1440,
2483 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
2484 receive_fee: transaction_fee,
2487 }))
2488 }
2489
2490 async fn send_payment_v2(
2493 &self,
2494 payload: SendPaymentPayload,
2495 ) -> Result<std::result::Result<[u8; 32], Signature>> {
2496 self.select_client(payload.federation_id)
2497 .await?
2498 .value()
2499 .get_first_module::<GatewayClientModuleV2>()
2500 .expect("Must have client module")
2501 .send_payment(payload)
2502 .await
2503 .map_err(LNv2Error::OutgoingPayment)
2504 .map_err(PublicGatewayError::LNv2)
2505 }
2506
2507 async fn create_bolt11_invoice_v2(
2512 &self,
2513 payload: CreateBolt11InvoicePayload,
2514 ) -> Result<Bolt11Invoice> {
2515 if !payload.contract.verify() {
2516 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2517 "The contract is invalid".to_string(),
2518 )));
2519 }
2520
2521 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
2522 LNv2Error::IncomingPayment(format!(
2523 "Federation {} does not exist",
2524 payload.federation_id
2525 )),
2526 )?;
2527
2528 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
2529 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2530 "The incoming contract is keyed to another gateway".to_string(),
2531 )));
2532 }
2533
2534 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
2535
2536 if contract_amount == Amount::ZERO {
2537 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2538 "Zero amount incoming contracts are not supported".to_string(),
2539 )));
2540 }
2541
2542 if contract_amount != payload.contract.commitment.amount {
2543 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2544 "The contract amount does not pay the correct amount of fees".to_string(),
2545 )));
2546 }
2547
2548 if payload.contract.commitment.expiration <= duration_since_epoch().as_secs() {
2549 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2550 "The contract has already expired".to_string(),
2551 )));
2552 }
2553
2554 let payment_hash = match payload.contract.commitment.payment_image {
2555 PaymentImage::Hash(payment_hash) => payment_hash,
2556 PaymentImage::Point(..) => {
2557 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2558 "PaymentImage is not a payment hash".to_string(),
2559 )));
2560 }
2561 };
2562
2563 let invoice = self
2564 .create_invoice_via_lnrpc_v2(
2565 payment_hash,
2566 payload.amount,
2567 payload.description.clone(),
2568 payload.expiry_secs,
2569 )
2570 .await?;
2571
2572 let mut dbtx = self.gateway_db.begin_transaction().await;
2573
2574 if dbtx
2575 .save_registered_incoming_contract(
2576 payload.federation_id,
2577 payload.amount,
2578 payload.contract,
2579 )
2580 .await
2581 .is_some()
2582 {
2583 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2584 "PaymentHash is already registered".to_string(),
2585 )));
2586 }
2587
2588 dbtx.commit_tx_result().await.map_err(|_| {
2589 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2590 "Payment hash is already registered".to_string(),
2591 ))
2592 })?;
2593
2594 Ok(invoice)
2595 }
2596
2597 pub async fn create_invoice_via_lnrpc_v2(
2600 &self,
2601 payment_hash: sha256::Hash,
2602 amount: Amount,
2603 description: Bolt11InvoiceDescription,
2604 expiry_time: u32,
2605 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
2606 let lnrpc = self.get_lightning_context().await?.lnrpc;
2607
2608 let response = match description {
2609 Bolt11InvoiceDescription::Direct(description) => {
2610 lnrpc
2611 .create_invoice(CreateInvoiceRequest {
2612 payment_hash: Some(payment_hash),
2613 amount_msat: amount.msats,
2614 expiry_secs: expiry_time,
2615 description: Some(InvoiceDescription::Direct(description)),
2616 })
2617 .await?
2618 }
2619 Bolt11InvoiceDescription::Hash(hash) => {
2620 lnrpc
2621 .create_invoice(CreateInvoiceRequest {
2622 payment_hash: Some(payment_hash),
2623 amount_msat: amount.msats,
2624 expiry_secs: expiry_time,
2625 description: Some(InvoiceDescription::Hash(hash)),
2626 })
2627 .await?
2628 }
2629 };
2630
2631 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
2632 LightningRpcError::FailedToGetInvoice {
2633 failure_reason: e.to_string(),
2634 }
2635 })
2636 }
2637
2638 pub async fn verify_bolt11_preimage_v2(
2639 &self,
2640 payment_hash: sha256::Hash,
2641 wait: bool,
2642 ) -> std::result::Result<VerifyResponse, String> {
2643 let registered_contract = self
2644 .gateway_db
2645 .begin_transaction_nc()
2646 .await
2647 .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
2648 .await
2649 .ok_or("Unknown payment hash".to_string())?;
2650
2651 let client = self
2652 .select_client(registered_contract.federation_id)
2653 .await
2654 .map_err(|_| "Not connected to federation".to_string())?
2655 .into_value();
2656
2657 let operation_id = OperationId::from_encodable(®istered_contract.contract);
2658
2659 if !(wait || client.operation_exists(operation_id).await) {
2660 return Ok(VerifyResponse {
2661 status: "OK".to_string(),
2662 settled: false,
2663 preimage: None,
2664 });
2665 }
2666
2667 let state = client
2668 .get_first_module::<GatewayClientModuleV2>()
2669 .expect("Must have client module")
2670 .await_receive(operation_id)
2671 .await;
2672
2673 let preimage = match state {
2674 FinalReceiveState::Success(preimage) => Ok(preimage),
2675 FinalReceiveState::Failure => Err("Payment has failed".to_string()),
2676 FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
2677 FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
2678 }?;
2679
2680 Ok(VerifyResponse {
2681 status: "OK".to_string(),
2682 settled: true,
2683 preimage: Some(preimage),
2684 })
2685 }
2686
2687 pub async fn get_registered_incoming_contract_and_client_v2(
2691 &self,
2692 payment_image: PaymentImage,
2693 amount_msats: u64,
2694 ) -> Result<(IncomingContract, ClientHandleArc)> {
2695 let registered_incoming_contract = self
2696 .gateway_db
2697 .begin_transaction_nc()
2698 .await
2699 .load_registered_incoming_contract(payment_image)
2700 .await
2701 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2702 "No corresponding decryption contract available".to_string(),
2703 )))?;
2704
2705 if registered_incoming_contract.incoming_amount_msats != amount_msats {
2706 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2707 "The available decryption contract's amount is not equal to the requested amount"
2708 .to_string(),
2709 )));
2710 }
2711
2712 let client = self
2713 .select_client(registered_incoming_contract.federation_id)
2714 .await?
2715 .into_value();
2716
2717 Ok((registered_incoming_contract.contract, client))
2718 }
2719}
2720
2721#[async_trait]
2722impl IGatewayClientV2 for Gateway {
2723 async fn complete_htlc(&self, htlc_response: InterceptPaymentResponse) {
2724 loop {
2725 match self.get_lightning_context().await {
2726 Ok(lightning_context) => {
2727 match lightning_context
2728 .lnrpc
2729 .complete_htlc(htlc_response.clone())
2730 .await
2731 {
2732 Ok(..) => return,
2733 Err(err) => {
2734 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2735 }
2736 }
2737 }
2738 Err(err) => {
2739 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2740 }
2741 }
2742
2743 sleep(Duration::from_secs(5)).await;
2744 }
2745 }
2746
2747 async fn is_direct_swap(
2748 &self,
2749 invoice: &Bolt11Invoice,
2750 ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>> {
2751 let lightning_context = self.get_lightning_context().await?;
2752 if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
2753 let (contract, client) = self
2754 .get_registered_incoming_contract_and_client_v2(
2755 PaymentImage::Hash(*invoice.payment_hash()),
2756 invoice
2757 .amount_milli_satoshis()
2758 .expect("The amount invoice has been previously checked"),
2759 )
2760 .await?;
2761 Ok(Some((contract, client)))
2762 } else {
2763 Ok(None)
2764 }
2765 }
2766
2767 async fn pay(
2768 &self,
2769 invoice: Bolt11Invoice,
2770 max_delay: u64,
2771 max_fee: Amount,
2772 ) -> std::result::Result<[u8; 32], LightningRpcError> {
2773 let lightning_context = self.get_lightning_context().await?;
2774 lightning_context
2775 .lnrpc
2776 .pay(invoice, max_delay, max_fee)
2777 .await
2778 .map(|response| response.preimage.0)
2779 }
2780
2781 async fn min_contract_amount(
2782 &self,
2783 federation_id: &FederationId,
2784 amount: u64,
2785 ) -> anyhow::Result<Amount> {
2786 Ok(self
2787 .routing_info_v2(federation_id)
2788 .await?
2789 .ok_or(anyhow!("Routing Info not available"))?
2790 .send_fee_minimum
2791 .add_to(amount))
2792 }
2793
2794 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
2795 let rhints = invoice.route_hints();
2796 match rhints.first().and_then(|rh| rh.0.last()) {
2797 None => None,
2798 Some(hop) => match self.get_lightning_context().await {
2799 Ok(lightning_context) => {
2800 if hop.src_node_id != lightning_context.lightning_public_key {
2801 return None;
2802 }
2803
2804 self.federation_manager
2805 .read()
2806 .await
2807 .get_client_for_index(hop.short_channel_id)
2808 }
2809 Err(_) => None,
2810 },
2811 }
2812 }
2813
2814 async fn relay_lnv1_swap(
2815 &self,
2816 client: &ClientHandleArc,
2817 invoice: &Bolt11Invoice,
2818 ) -> anyhow::Result<FinalReceiveState> {
2819 let swap_params = SwapParameters {
2820 payment_hash: *invoice.payment_hash(),
2821 amount_msat: Amount::from_msats(
2822 invoice
2823 .amount_milli_satoshis()
2824 .ok_or(anyhow!("Amountless invoice not supported"))?,
2825 ),
2826 };
2827 let lnv1 = client
2828 .get_first_module::<GatewayClientModule>()
2829 .expect("No LNv1 module");
2830 let operation_id = lnv1.gateway_handle_direct_swap(swap_params).await?;
2831 let mut stream = lnv1
2832 .gateway_subscribe_ln_receive(operation_id)
2833 .await?
2834 .into_stream();
2835 let mut final_state = FinalReceiveState::Failure;
2836 while let Some(update) = stream.next().await {
2837 match update {
2838 GatewayExtReceiveStates::Funding => {}
2839 GatewayExtReceiveStates::FundingFailed { error: _ } => {
2840 final_state = FinalReceiveState::Rejected;
2841 }
2842 GatewayExtReceiveStates::Preimage(preimage) => {
2843 final_state = FinalReceiveState::Success(preimage.0);
2844 }
2845 GatewayExtReceiveStates::RefundError {
2846 error_message: _,
2847 error: _,
2848 } => {
2849 final_state = FinalReceiveState::Failure;
2850 }
2851 GatewayExtReceiveStates::RefundSuccess {
2852 out_points: _,
2853 error: _,
2854 } => {
2855 final_state = FinalReceiveState::Refunded;
2856 }
2857 }
2858 }
2859
2860 Ok(final_state)
2861 }
2862}
2863
2864#[async_trait]
2865impl IGatewayClientV1 for Gateway {
2866 async fn verify_preimage_authentication(
2867 &self,
2868 payment_hash: sha256::Hash,
2869 preimage_auth: sha256::Hash,
2870 contract: OutgoingContractAccount,
2871 ) -> std::result::Result<(), OutgoingPaymentError> {
2872 let mut dbtx = self.gateway_db.begin_transaction().await;
2873 if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
2874 if secret_hash != preimage_auth {
2875 return Err(OutgoingPaymentError {
2876 error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
2877 contract_id: contract.contract.contract_id(),
2878 contract: Some(contract),
2879 });
2880 }
2881 } else {
2882 dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
2885 .await;
2886 return dbtx
2887 .commit_tx_result()
2888 .await
2889 .map_err(|_| OutgoingPaymentError {
2890 error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
2891 contract_id: contract.contract.contract_id(),
2892 contract: Some(contract),
2893 });
2894 }
2895
2896 Ok(())
2897 }
2898
2899 async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()> {
2900 let lightning_context = self.get_lightning_context().await?;
2901
2902 if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
2903 ensure!(
2904 lightning_context.lnrpc.supports_private_payments(),
2905 "Private payments are not supported by the lightning node"
2906 );
2907 }
2908
2909 Ok(())
2910 }
2911
2912 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
2913 let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
2914 gateway_dbtx
2915 .load_federation_config(federation_id)
2916 .await
2917 .map(|c| c.lightning_fee.into())
2918 }
2919
2920 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
2921 self.federation_manager
2922 .read()
2923 .await
2924 .client(federation_id)
2925 .cloned()
2926 }
2927
2928 async fn get_client_for_invoice(
2929 &self,
2930 payment_data: PaymentData,
2931 ) -> Option<Spanned<ClientHandleArc>> {
2932 let rhints = payment_data.route_hints();
2933 match rhints.first().and_then(|rh| rh.0.last()) {
2934 None => None,
2935 Some(hop) => match self.get_lightning_context().await {
2936 Ok(lightning_context) => {
2937 if hop.src_node_id != lightning_context.lightning_public_key {
2938 return None;
2939 }
2940
2941 self.federation_manager
2942 .read()
2943 .await
2944 .get_client_for_index(hop.short_channel_id)
2945 }
2946 Err(_) => None,
2947 },
2948 }
2949 }
2950
2951 async fn pay(
2952 &self,
2953 payment_data: PaymentData,
2954 max_delay: u64,
2955 max_fee: Amount,
2956 ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
2957 let lightning_context = self.get_lightning_context().await?;
2958
2959 match payment_data {
2960 PaymentData::Invoice(invoice) => {
2961 lightning_context
2962 .lnrpc
2963 .pay(invoice, max_delay, max_fee)
2964 .await
2965 }
2966 PaymentData::PrunedInvoice(invoice) => {
2967 lightning_context
2968 .lnrpc
2969 .pay_private(invoice, max_delay, max_fee)
2970 .await
2971 }
2972 }
2973 }
2974
2975 async fn complete_htlc(
2976 &self,
2977 htlc: InterceptPaymentResponse,
2978 ) -> std::result::Result<(), LightningRpcError> {
2979 let lightning_context = loop {
2981 match self.get_lightning_context().await {
2982 Ok(lightning_context) => break lightning_context,
2983 Err(err) => {
2984 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2985 sleep(Duration::from_secs(5)).await;
2986 }
2987 }
2988 };
2989
2990 lightning_context.lnrpc.complete_htlc(htlc).await
2991 }
2992
2993 async fn is_lnv2_direct_swap(
2994 &self,
2995 payment_hash: sha256::Hash,
2996 amount: Amount,
2997 ) -> anyhow::Result<
2998 Option<(
2999 fedimint_lnv2_common::contracts::IncomingContract,
3000 ClientHandleArc,
3001 )>,
3002 > {
3003 let (contract, client) = self
3004 .get_registered_incoming_contract_and_client_v2(
3005 PaymentImage::Hash(payment_hash),
3006 amount.msats,
3007 )
3008 .await?;
3009 Ok(Some((contract, client)))
3010 }
3011}