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;
23pub mod rpc_server;
24mod types;
25
26use std::collections::{BTreeMap, BTreeSet};
27use std::env;
28use std::fmt::Display;
29use std::net::SocketAddr;
30use std::str::FromStr;
31use std::sync::Arc;
32use std::time::{Duration, UNIX_EPOCH};
33
34use anyhow::{Context, anyhow, ensure};
35use async_trait::async_trait;
36use bitcoin::hashes::sha256;
37use bitcoin::{Address, Network, Txid};
38use clap::Parser;
39use client::GatewayClientBuilder;
40pub use config::GatewayParameters;
41use config::{DatabaseBackend, GatewayOpts};
42use envs::FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV;
43use error::FederationNotConnected;
44use events::ALL_GATEWAY_EVENTS;
45use federation_manager::FederationManager;
46use fedimint_api_client::api::net::Connector;
47use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic};
48use fedimint_bitcoind::bitcoincore::BitcoindClient;
49use fedimint_bitcoind::{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::{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, CreateInvoiceForOperatorPayload, CreateOfferPayload, CreateOfferResponse,
73 DepositAddressPayload, DepositAddressRecheckPayload, FederationBalanceInfo, FederationConfig,
74 FederationInfo, GatewayBalances, GatewayFedConfig, GatewayInfo, GetInvoiceRequest,
75 GetInvoiceResponse, LeaveFedPayload, LightningMode, ListTransactionsPayload,
76 ListTransactionsResponse, MnemonicResponse, OpenChannelRequest, PayInvoiceForOperatorPayload,
77 PayOfferPayload, PayOfferResponse, PaymentLogPayload, PaymentLogResponse, PaymentStats,
78 PaymentSummaryPayload, PaymentSummaryResponse, ReceiveEcashPayload, ReceiveEcashResponse,
79 SendOnchainRequest, SetFeesPayload, SpendEcashPayload, SpendEcashResponse, V1_API_ENDPOINT,
80 WithdrawPayload, WithdrawResponse,
81};
82use fedimint_gateway_server_db::{GatewayDbtxNcExt as _, get_gatewayd_database_migrations};
83use fedimint_gw_client::events::compute_lnv1_stats;
84use fedimint_gw_client::pay::{OutgoingPaymentError, OutgoingPaymentErrorType};
85use fedimint_gw_client::{
86 GatewayClientModule, GatewayExtPayStates, GatewayExtReceiveStates, IGatewayClientV1,
87 SwapParameters,
88};
89use fedimint_gwv2_client::events::compute_lnv2_stats;
90use fedimint_gwv2_client::{
91 EXPIRATION_DELTA_MINIMUM_V2, FinalReceiveState, GatewayClientModuleV2, IGatewayClientV2,
92};
93use fedimint_lightning::lnd::GatewayLndClient;
94use fedimint_lightning::{
95 CreateInvoiceRequest, ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse,
96 InvoiceDescription, LightningContext, LightningRpcError, PayInvoiceResponse, PaymentAction,
97 RouteHtlcStream, ldk,
98};
99use fedimint_ln_client::pay::PaymentData;
100use fedimint_ln_common::LightningCommonInit;
101use fedimint_ln_common::config::LightningClientConfig;
102use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
103use fedimint_ln_common::contracts::{IdentifiableContract, Preimage};
104use fedimint_lnv2_common::Bolt11InvoiceDescription;
105use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
106use fedimint_lnv2_common::gateway_api::{
107 CreateBolt11InvoicePayload, PaymentFee, RoutingInfo, SendPaymentPayload,
108};
109use fedimint_lnv2_common::lnurl::VerifyResponse;
110use fedimint_logging::LOG_GATEWAY;
111use fedimint_mint_client::{
112 MintClientInit, MintClientModule, SelectNotesWithAtleastAmount, SelectNotesWithExactAmount,
113};
114use fedimint_wallet_client::{WalletClientInit, WalletClientModule, WithdrawState};
115use futures::stream::StreamExt;
116use lightning_invoice::{Bolt11Invoice, RoutingFees};
117use rand::rngs::OsRng;
118use tokio::sync::RwLock;
119use tracing::{debug, info, info_span, warn};
120
121use crate::envs::FM_GATEWAY_MNEMONIC_ENV;
122use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError};
123use crate::events::get_events_for_duration;
124use crate::rpc_server::run_webserver;
125use crate::types::PrettyInterceptPaymentRequest;
126
127const GW_ANNOUNCEMENT_TTL: Duration = Duration::from_secs(600);
129
130const DEFAULT_NUM_ROUTE_HINTS: u32 = 1;
133
134pub const DEFAULT_NETWORK: Network = Network::Regtest;
136
137pub type Result<T> = std::result::Result<T, PublicGatewayError>;
138pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
139
140const DB_FILE: &str = "gatewayd.db";
143
144const LDK_NODE_DB_FOLDER: &str = "ldk_node";
147
148#[cfg_attr(doc, aquamarine::aquamarine)]
149#[derive(Clone, Debug)]
161pub enum GatewayState {
162 Disconnected,
163 Syncing,
164 Connected,
165 Running { lightning_context: LightningContext },
166 ShuttingDown { lightning_context: LightningContext },
167}
168
169impl Display for GatewayState {
170 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
171 match self {
172 GatewayState::Disconnected => write!(f, "Disconnected"),
173 GatewayState::Syncing => write!(f, "Syncing"),
174 GatewayState::Connected => write!(f, "Connected"),
175 GatewayState::Running { .. } => write!(f, "Running"),
176 GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"),
177 }
178 }
179}
180
181enum ReceivePaymentStreamAction {
183 RetryAfterDelay,
184 NoRetry,
185}
186
187#[derive(Clone)]
188pub struct Gateway {
189 federation_manager: Arc<RwLock<FederationManager>>,
191
192 mnemonic: Mnemonic,
194
195 lightning_mode: LightningMode,
197
198 state: Arc<RwLock<GatewayState>>,
200
201 client_builder: GatewayClientBuilder,
204
205 gateway_db: Database,
207
208 gateway_id: PublicKey,
211
212 versioned_api: SafeUrl,
214
215 listen: SocketAddr,
217
218 task_group: TaskGroup,
220
221 bcrypt_password_hash: Arc<bcrypt::HashParts>,
224
225 num_route_hints: u32,
227
228 network: Network,
230
231 chain_source: ChainSource,
233
234 default_routing_fees: PaymentFee,
236
237 default_transaction_fees: PaymentFee,
239}
240
241impl std::fmt::Debug for Gateway {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 f.debug_struct("Gateway")
244 .field("federation_manager", &self.federation_manager)
245 .field("state", &self.state)
246 .field("client_builder", &self.client_builder)
247 .field("gateway_db", &self.gateway_db)
248 .field("gateway_id", &self.gateway_id)
249 .field("versioned_api", &self.versioned_api)
250 .field("listen", &self.listen)
251 .finish_non_exhaustive()
252 }
253}
254
255impl Gateway {
256 #[allow(clippy::too_many_arguments)]
259 pub async fn new_with_custom_registry(
260 lightning_mode: LightningMode,
261 client_builder: GatewayClientBuilder,
262 listen: SocketAddr,
263 api_addr: SafeUrl,
264 bcrypt_password_hash: bcrypt::HashParts,
265 network: Network,
266 num_route_hints: u32,
267 gateway_db: Database,
268 gateway_state: GatewayState,
269 chain_source: ChainSource,
270 ) -> anyhow::Result<Gateway> {
271 let versioned_api = api_addr
272 .join(V1_API_ENDPOINT)
273 .expect("Failed to version gateway API address");
274 Gateway::new(
275 lightning_mode,
276 GatewayParameters {
277 listen,
278 versioned_api,
279 bcrypt_password_hash,
280 network,
281 num_route_hints,
282 default_routing_fees: PaymentFee::TRANSACTION_FEE_DEFAULT,
283 default_transaction_fees: PaymentFee::TRANSACTION_FEE_DEFAULT,
284 },
285 gateway_db,
286 client_builder,
287 gateway_state,
288 chain_source,
289 )
290 .await
291 }
292
293 fn get_bitcoind_client(
296 opts: &GatewayOpts,
297 network: bitcoin::Network,
298 gateway_id: &PublicKey,
299 ) -> anyhow::Result<(BitcoindClient, ChainSource)> {
300 let bitcoind_username = opts
301 .bitcoind_username
302 .clone()
303 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
304 let url = opts.bitcoind_url.clone().expect("No bitcoind url set");
305 let password = opts
306 .bitcoind_password
307 .clone()
308 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
309
310 let chain_source = ChainSource::Bitcoind {
311 username: bitcoind_username.clone(),
312 password: password.clone(),
313 server_url: url.clone(),
314 };
315 let wallet_name = format!("gatewayd-{gateway_id}");
316 let client = BitcoindClient::new(&url, bitcoind_username, password, &wallet_name, network)?;
317 Ok((client, chain_source))
318 }
319
320 pub async fn new_with_default_modules() -> anyhow::Result<Gateway> {
323 let opts = GatewayOpts::parse();
324 let gateway_parameters = opts.to_gateway_parameters()?;
325 let decoders = ModuleDecoderRegistry::default();
326
327 let db_path = opts.data_dir.join(DB_FILE);
328 let gateway_db = match opts.db_backend {
329 DatabaseBackend::RocksDb => {
330 debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
331 Database::new(fedimint_rocksdb::RocksDb::open(db_path).await?, decoders)
332 }
333 DatabaseBackend::CursedRedb => {
334 debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
335 Database::new(
336 fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
337 decoders,
338 )
339 }
340 };
341
342 let gateway_id = Self::load_or_create_gateway_id(&gateway_db).await;
343 let (dyn_bitcoin_rpc, chain_source) =
344 match (opts.bitcoind_url.as_ref(), opts.esplora_url.as_ref()) {
345 (Some(_), None) => {
346 let (client, chain_source) =
347 Self::get_bitcoind_client(&opts, gateway_parameters.network, &gateway_id)?;
348 (client.into_dyn(), chain_source)
349 }
350 (None, Some(url)) => {
351 let client = EsploraClient::new(url)
352 .expect("Could not create EsploraClient")
353 .into_dyn();
354 let chain_source = ChainSource::Esplora {
355 server_url: url.clone(),
356 };
357 (client, chain_source)
358 }
359 (Some(_), Some(_)) => {
360 let (client, chain_source) =
362 Self::get_bitcoind_client(&opts, gateway_parameters.network, &gateway_id)?;
363 (client.into_dyn(), chain_source)
364 }
365 _ => unreachable!("ArgGroup already enforced XOR relation"),
366 };
367
368 let mut registry = ClientModuleInitRegistry::new();
371 registry.attach(MintClientInit);
372 registry.attach(WalletClientInit::new(dyn_bitcoin_rpc));
373
374 let client_builder =
375 GatewayClientBuilder::new(opts.data_dir.clone(), registry, opts.db_backend);
376
377 info!(
378 target: LOG_GATEWAY,
379 version = %fedimint_build_code_version_env!(),
380 "Starting gatewayd",
381 );
382
383 Gateway::new(
384 opts.mode,
385 gateway_parameters,
386 gateway_db,
387 client_builder,
388 GatewayState::Disconnected,
389 chain_source,
390 )
391 .await
392 }
393
394 async fn new(
397 lightning_mode: LightningMode,
398 gateway_parameters: GatewayParameters,
399 gateway_db: Database,
400 client_builder: GatewayClientBuilder,
401 gateway_state: GatewayState,
402 chain_source: ChainSource,
403 ) -> anyhow::Result<Gateway> {
404 apply_migrations(
407 &gateway_db,
408 (),
409 "gatewayd".to_string(),
410 get_gatewayd_database_migrations(),
411 None,
412 None,
413 )
414 .await?;
415
416 let num_route_hints = gateway_parameters.num_route_hints;
417 let network = gateway_parameters.network;
418
419 let task_group = TaskGroup::new();
420 task_group.install_kill_handler();
421
422 Ok(Self {
423 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
424 mnemonic: Self::load_or_generate_mnemonic(&gateway_db).await?,
425 lightning_mode,
426 state: Arc::new(RwLock::new(gateway_state)),
427 client_builder,
428 gateway_id: Self::load_or_create_gateway_id(&gateway_db).await,
429 gateway_db,
430 versioned_api: gateway_parameters.versioned_api,
431 listen: gateway_parameters.listen,
432 task_group,
433 bcrypt_password_hash: Arc::new(gateway_parameters.bcrypt_password_hash),
434 num_route_hints,
435 network,
436 chain_source,
437 default_routing_fees: gateway_parameters.default_routing_fees,
438 default_transaction_fees: gateway_parameters.default_transaction_fees,
439 })
440 }
441
442 async fn load_or_create_gateway_id(gateway_db: &Database) -> PublicKey {
444 let mut dbtx = gateway_db.begin_transaction().await;
445 let keypair = dbtx.load_or_create_gateway_keypair().await;
446 dbtx.commit_tx().await;
447 keypair.public_key()
448 }
449
450 pub fn gateway_id(&self) -> PublicKey {
451 self.gateway_id
452 }
453
454 pub fn versioned_api(&self) -> &SafeUrl {
455 &self.versioned_api
456 }
457
458 async fn get_state(&self) -> GatewayState {
459 self.state.read().await.clone()
460 }
461
462 pub async fn dump_database(
465 dbtx: &mut DatabaseTransaction<'_>,
466 prefix_names: Vec<String>,
467 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
468 dbtx.dump_database(prefix_names).await
469 }
470
471 pub async fn run(
476 self,
477 runtime: Arc<tokio::runtime::Runtime>,
478 ) -> anyhow::Result<TaskShutdownToken> {
479 install_crypto_provider().await;
480 self.register_clients_timer();
481 self.load_clients().await?;
482 self.start_gateway(runtime);
483 let handle = self.task_group.make_handle();
485 run_webserver(Arc::new(self)).await?;
486 let shutdown_receiver = handle.make_shutdown_rx();
487 Ok(shutdown_receiver)
488 }
489
490 fn start_gateway(&self, runtime: Arc<tokio::runtime::Runtime>) {
493 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 5;
494
495 let self_copy = self.clone();
496 let tg = self.task_group.clone();
497 self.task_group.spawn(
498 "Subscribe to intercepted lightning payments in stream",
499 |handle| async move {
500 loop {
502 if handle.is_shutting_down() {
503 info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
504 break;
505 }
506
507 let payment_stream_task_group = tg.make_subgroup();
508 let lnrpc_route = self_copy.create_lightning_client(runtime.clone());
509
510 debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
511 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
512 {
513 Ok((stream, ln_client)) => (stream, ln_client),
514 Err(err) => {
515 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
516 continue
517 }
518 };
519
520 self_copy.set_gateway_state(GatewayState::Connected).await;
522 info!(target: LOG_GATEWAY, "Established lightning payment stream");
523
524 let route_payments_response =
525 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
526
527 self_copy.set_gateway_state(GatewayState::Disconnected).await;
528 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
529 crit!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Lightning payment stream task group shutdown");
530 }
531
532 self_copy.unannounce_from_all_federations().await;
533
534 match route_payments_response {
535 ReceivePaymentStreamAction::RetryAfterDelay => {
536 warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
537 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
538 }
539 ReceivePaymentStreamAction::NoRetry => break,
540 }
541 }
542 },
543 );
544 }
545
546 async fn route_lightning_payments<'a>(
550 &'a self,
551 handle: &TaskHandle,
552 mut stream: RouteHtlcStream<'a>,
553 ln_client: Arc<dyn ILnRpcClient>,
554 ) -> ReceivePaymentStreamAction {
555 let (lightning_public_key, lightning_alias, lightning_network, synced_to_chain) =
556 match ln_client.parsed_node_info().await {
557 Ok((
558 lightning_public_key,
559 lightning_alias,
560 lightning_network,
561 _block_height,
562 synced_to_chain,
563 )) => (
564 lightning_public_key,
565 lightning_alias,
566 lightning_network,
567 synced_to_chain,
568 ),
569 Err(err) => {
570 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to retrieve Lightning info");
571 return ReceivePaymentStreamAction::RetryAfterDelay;
572 }
573 };
574
575 assert!(
576 self.network == lightning_network,
577 "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
578 self.network
579 );
580
581 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
582 info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
583 } else {
584 self.set_gateway_state(GatewayState::Syncing).await;
585 info!(target: LOG_GATEWAY, "Waiting for chain sync");
586 if let Err(err) = ln_client.wait_for_chain_sync().await {
587 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
588 return ReceivePaymentStreamAction::RetryAfterDelay;
589 }
590 }
591
592 let lightning_context = LightningContext {
593 lnrpc: ln_client,
594 lightning_public_key,
595 lightning_alias,
596 lightning_network,
597 };
598 self.set_gateway_state(GatewayState::Running { lightning_context })
599 .await;
600 info!(target: LOG_GATEWAY, "Gateway is running");
601
602 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
603 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
606 let all_federations_configs =
607 dbtx.load_federation_configs().await.into_iter().collect();
608 self.register_federations(&all_federations_configs, &self.task_group)
609 .await;
610 }
611
612 if handle
615 .cancel_on_shutdown(async move {
616 loop {
617 let payment_request_or = tokio::select! {
618 payment_request_or = stream.next() => {
619 payment_request_or
620 }
621 () = self.is_shutting_down_safely() => {
622 break;
623 }
624 };
625
626 let Some(payment_request) = payment_request_or else {
627 warn!(
628 target: LOG_GATEWAY,
629 "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
630 );
631 break;
632 };
633
634 let state_guard = self.state.read().await;
635 if let GatewayState::Running { ref lightning_context } = *state_guard {
636 self.handle_lightning_payment(payment_request, lightning_context).await;
637 } else {
638 warn!(
639 target: LOG_GATEWAY,
640 state = %state_guard,
641 "Gateway isn't in a running state, cannot handle incoming payments."
642 );
643 break;
644 }
645 }
646 })
647 .await
648 .is_ok()
649 {
650 warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
651 ReceivePaymentStreamAction::RetryAfterDelay
652 } else {
653 info!(target: LOG_GATEWAY, "Received shutdown signal");
654 ReceivePaymentStreamAction::NoRetry
655 }
656 }
657
658 async fn is_shutting_down_safely(&self) {
661 loop {
662 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
663 return;
664 }
665
666 fedimint_core::task::sleep(Duration::from_secs(1)).await;
667 }
668 }
669
670 async fn handle_lightning_payment(
675 &self,
676 payment_request: InterceptPaymentRequest,
677 lightning_context: &LightningContext,
678 ) {
679 info!(
680 target: LOG_GATEWAY,
681 lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
682 "Intercepting lightning payment",
683 );
684
685 if self
686 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
687 .await
688 .is_ok()
689 {
690 return;
691 }
692
693 if self
694 .try_handle_lightning_payment_ln_legacy(&payment_request)
695 .await
696 .is_ok()
697 {
698 return;
699 }
700
701 Self::forward_lightning_payment(payment_request, lightning_context).await;
702 }
703
704 async fn try_handle_lightning_payment_lnv2(
707 &self,
708 htlc_request: &InterceptPaymentRequest,
709 lightning_context: &LightningContext,
710 ) -> Result<()> {
711 let (contract, client) = self
717 .get_registered_incoming_contract_and_client_v2(
718 PaymentImage::Hash(htlc_request.payment_hash),
719 htlc_request.amount_msat,
720 )
721 .await?;
722
723 if let Err(err) = client
724 .get_first_module::<GatewayClientModuleV2>()
725 .expect("Must have client module")
726 .relay_incoming_htlc(
727 htlc_request.payment_hash,
728 htlc_request.incoming_chan_id,
729 htlc_request.htlc_id,
730 contract,
731 htlc_request.amount_msat,
732 )
733 .await
734 {
735 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error relaying incoming lightning payment");
736
737 let outcome = InterceptPaymentResponse {
738 action: PaymentAction::Cancel,
739 payment_hash: htlc_request.payment_hash,
740 incoming_chan_id: htlc_request.incoming_chan_id,
741 htlc_id: htlc_request.htlc_id,
742 };
743
744 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
745 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
746 }
747 }
748
749 Ok(())
750 }
751
752 async fn try_handle_lightning_payment_ln_legacy(
755 &self,
756 htlc_request: &InterceptPaymentRequest,
757 ) -> Result<()> {
758 let Some(federation_index) = htlc_request.short_channel_id else {
760 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
761 "Incoming payment has not last hop short channel id".to_string(),
762 )));
763 };
764
765 let Some(client) = self
766 .federation_manager
767 .read()
768 .await
769 .get_client_for_index(federation_index)
770 else {
771 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())));
772 };
773
774 client
775 .borrow()
776 .with(|client| async {
777 let htlc = htlc_request.clone().try_into();
778 match htlc {
779 Ok(htlc) => {
780 match client
781 .get_first_module::<GatewayClientModule>()
782 .expect("Must have client module")
783 .gateway_handle_intercepted_htlc(htlc)
784 .await
785 {
786 Ok(_) => Ok(()),
787 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
788 format!("Error intercepting lightning payment {e:?}"),
789 ))),
790 }
791 }
792 _ => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
793 "Could not convert InterceptHtlcResult into an HTLC".to_string(),
794 ))),
795 }
796 })
797 .await
798 }
799
800 async fn forward_lightning_payment(
804 htlc_request: InterceptPaymentRequest,
805 lightning_context: &LightningContext,
806 ) {
807 let outcome = InterceptPaymentResponse {
808 action: PaymentAction::Forward,
809 payment_hash: htlc_request.payment_hash,
810 incoming_chan_id: htlc_request.incoming_chan_id,
811 htlc_id: htlc_request.htlc_id,
812 };
813
814 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
815 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
816 }
817 }
818
819 async fn set_gateway_state(&self, state: GatewayState) {
821 let mut lock = self.state.write().await;
822 *lock = state;
823 }
824
825 pub async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
828 let GatewayState::Running { lightning_context } = self.get_state().await else {
829 return Ok(GatewayInfo {
830 federations: vec![],
831 federation_fake_scids: None,
832 version_hash: fedimint_build_code_version_env!().to_string(),
833 lightning_pub_key: None,
834 lightning_alias: None,
835 gateway_id: self.gateway_id,
836 gateway_state: self.state.read().await.to_string(),
837 network: self.network,
838 block_height: None,
839 synced_to_chain: false,
840 api: self.versioned_api.clone(),
841 lightning_mode: self.lightning_mode.clone(),
842 });
843 };
844
845 let dbtx = self.gateway_db.begin_transaction_nc().await;
846 let federations = self
847 .federation_manager
848 .read()
849 .await
850 .federation_info_all_federations(dbtx)
851 .await;
852
853 let channels: BTreeMap<u64, FederationId> = federations
854 .iter()
855 .map(|federation_info| {
856 (
857 federation_info.config.federation_index,
858 federation_info.federation_id,
859 )
860 })
861 .collect();
862
863 let node_info = lightning_context.lnrpc.parsed_node_info().await?;
864
865 Ok(GatewayInfo {
866 federations,
867 federation_fake_scids: Some(channels),
868 version_hash: fedimint_build_code_version_env!().to_string(),
869 lightning_pub_key: Some(lightning_context.lightning_public_key.to_string()),
870 lightning_alias: Some(lightning_context.lightning_alias.clone()),
871 gateway_id: self.gateway_id,
872 gateway_state: self.state.read().await.to_string(),
873 network: self.network,
874 block_height: Some(node_info.3),
875 synced_to_chain: node_info.4,
876 api: self.versioned_api.clone(),
877 lightning_mode: self.lightning_mode.clone(),
878 })
879 }
880
881 pub async fn handle_get_federation_config(
884 &self,
885 federation_id_or: Option<FederationId>,
886 ) -> AdminResult<GatewayFedConfig> {
887 if !matches!(self.get_state().await, GatewayState::Running { .. }) {
888 return Ok(GatewayFedConfig {
889 federations: BTreeMap::new(),
890 });
891 }
892
893 let federations = if let Some(federation_id) = federation_id_or {
894 let mut federations = BTreeMap::new();
895 federations.insert(
896 federation_id,
897 self.federation_manager
898 .read()
899 .await
900 .get_federation_config(federation_id)
901 .await?,
902 );
903 federations
904 } else {
905 self.federation_manager
906 .read()
907 .await
908 .get_all_federation_configs()
909 .await
910 };
911
912 Ok(GatewayFedConfig { federations })
913 }
914
915 pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
918 let (_, address, _) = self
919 .select_client(payload.federation_id)
920 .await?
921 .value()
922 .get_first_module::<WalletClientModule>()
923 .expect("Must have client module")
924 .allocate_deposit_address_expert_only(())
925 .await?;
926 Ok(address)
927 }
928
929 pub async fn handle_withdraw_msg(
932 &self,
933 payload: WithdrawPayload,
934 ) -> AdminResult<WithdrawResponse> {
935 let WithdrawPayload {
936 amount,
937 address,
938 federation_id,
939 } = payload;
940
941 let address_network = get_network_for_address(&address);
942 let gateway_network = self.network;
943 let Ok(address) = address.require_network(gateway_network) else {
944 return Err(AdminGatewayError::WithdrawError {
945 failure_reason: format!(
946 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
947 ),
948 });
949 };
950
951 let client = self.select_client(federation_id).await?;
952 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
953
954 let (amount, fees) = match amount {
956 BitcoinAmountOrAll::All => {
959 let balance = bitcoin::Amount::from_sat(
960 client
961 .value()
962 .get_balance_for_btc()
963 .await
964 .map_err(|err| {
965 AdminGatewayError::Unexpected(anyhow!(
966 "Balance not available: {}",
967 err.fmt_compact_anyhow()
968 ))
969 })?
970 .msats
971 / 1000,
972 );
973 let fees = wallet_module.get_withdraw_fees(&address, balance).await?;
974 let withdraw_amount = balance.checked_sub(fees.amount());
975 if withdraw_amount.is_none() {
976 return Err(AdminGatewayError::WithdrawError {
977 failure_reason: format!(
978 "Insufficient funds. Balance: {balance} Fees: {fees:?}"
979 ),
980 });
981 }
982 (withdraw_amount.unwrap(), fees)
983 }
984 BitcoinAmountOrAll::Amount(amount) => (
985 amount,
986 wallet_module.get_withdraw_fees(&address, amount).await?,
987 ),
988 };
989
990 let operation_id = wallet_module.withdraw(&address, amount, fees, ()).await?;
991 let mut updates = wallet_module
992 .subscribe_withdraw_updates(operation_id)
993 .await?
994 .into_stream();
995
996 while let Some(update) = updates.next().await {
997 match update {
998 WithdrawState::Succeeded(txid) => {
999 info!(target: LOG_GATEWAY, amount = %amount, address = %address, "Sent funds");
1000 return Ok(WithdrawResponse { txid, fees });
1001 }
1002 WithdrawState::Failed(e) => {
1003 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
1004 }
1005 WithdrawState::Created => {}
1006 }
1007 }
1008
1009 Err(AdminGatewayError::WithdrawError {
1010 failure_reason: "Ran out of state updates while withdrawing".to_string(),
1011 })
1012 }
1013
1014 async fn handle_create_invoice_for_operator_msg(
1017 &self,
1018 payload: CreateInvoiceForOperatorPayload,
1019 ) -> AdminResult<Bolt11Invoice> {
1020 let GatewayState::Running { lightning_context } = self.get_state().await else {
1021 return Err(AdminGatewayError::Lightning(
1022 LightningRpcError::FailedToConnect,
1023 ));
1024 };
1025
1026 Bolt11Invoice::from_str(
1027 &lightning_context
1028 .lnrpc
1029 .create_invoice(CreateInvoiceRequest {
1030 payment_hash: None, amount_msat: payload.amount_msats,
1033 expiry_secs: payload.expiry_secs.unwrap_or(3600),
1034 description: payload.description.map(InvoiceDescription::Direct),
1035 })
1036 .await?
1037 .invoice,
1038 )
1039 .map_err(|e| {
1040 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1041 failure_reason: e.to_string(),
1042 })
1043 })
1044 }
1045
1046 async fn handle_pay_invoice_for_operator_msg(
1049 &self,
1050 payload: PayInvoiceForOperatorPayload,
1051 ) -> AdminResult<Preimage> {
1052 const BASE_FEE: u64 = 50;
1054 const FEE_DENOMINATOR: u64 = 100;
1055 const MAX_DELAY: u64 = 1008;
1056
1057 let GatewayState::Running { lightning_context } = self.get_state().await else {
1058 return Err(AdminGatewayError::Lightning(
1059 LightningRpcError::FailedToConnect,
1060 ));
1061 };
1062
1063 let max_fee = BASE_FEE
1064 + payload
1065 .invoice
1066 .amount_milli_satoshis()
1067 .context("Invoice is missing amount")?
1068 .saturating_div(FEE_DENOMINATOR);
1069
1070 let res = lightning_context
1071 .lnrpc
1072 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
1073 .await?;
1074 Ok(res.preimage)
1075 }
1076
1077 async fn handle_pay_invoice_msg(
1080 &self,
1081 payload: fedimint_ln_client::pay::PayInvoicePayload,
1082 ) -> Result<Preimage> {
1083 let GatewayState::Running { .. } = self.get_state().await else {
1084 return Err(PublicGatewayError::Lightning(
1085 LightningRpcError::FailedToConnect,
1086 ));
1087 };
1088
1089 debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1090 let client = self.select_client(payload.federation_id).await?;
1091 let contract_id = payload.contract_id;
1092 let gateway_module = &client
1093 .value()
1094 .get_first_module::<GatewayClientModule>()
1095 .map_err(LNv1Error::OutgoingPayment)
1096 .map_err(PublicGatewayError::LNv1)?;
1097 let operation_id = gateway_module
1098 .gateway_pay_bolt11_invoice(payload)
1099 .await
1100 .map_err(LNv1Error::OutgoingPayment)
1101 .map_err(PublicGatewayError::LNv1)?;
1102 let mut updates = gateway_module
1103 .gateway_subscribe_ln_pay(operation_id)
1104 .await
1105 .map_err(LNv1Error::OutgoingPayment)
1106 .map_err(PublicGatewayError::LNv1)?
1107 .into_stream();
1108 while let Some(update) = updates.next().await {
1109 match update {
1110 GatewayExtPayStates::Success { preimage, .. } => {
1111 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1112 return Ok(preimage);
1113 }
1114 GatewayExtPayStates::Fail {
1115 error,
1116 error_message,
1117 } => {
1118 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1119 error: Box::new(error),
1120 message: format!(
1121 "{error_message} while paying invoice with contract id {contract_id}"
1122 ),
1123 }));
1124 }
1125 GatewayExtPayStates::Canceled { error } => {
1126 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1127 error: Box::new(error.clone()),
1128 message: format!(
1129 "Cancelled with {error} while paying invoice with contract id {contract_id}"
1130 ),
1131 }));
1132 }
1133 GatewayExtPayStates::Created => {
1134 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1135 }
1136 other => {
1137 debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1138 }
1139 }
1140 }
1141
1142 Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1143 anyhow!("Ran out of state updates while paying invoice"),
1144 )))
1145 }
1146
1147 pub async fn handle_connect_federation(
1152 &self,
1153 payload: ConnectFedPayload,
1154 ) -> AdminResult<FederationInfo> {
1155 let GatewayState::Running { lightning_context } = self.get_state().await else {
1156 return Err(AdminGatewayError::Lightning(
1157 LightningRpcError::FailedToConnect,
1158 ));
1159 };
1160
1161 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
1162 AdminGatewayError::ClientCreationError(anyhow!(format!(
1163 "Invalid federation member string {e:?}"
1164 )))
1165 })?;
1166
1167 #[cfg(feature = "tor")]
1168 let connector = match &payload.use_tor {
1169 Some(true) => Connector::tor(),
1170 Some(false) => Connector::default(),
1171 None => {
1172 debug!(target: LOG_GATEWAY, "Missing `use_tor` payload field, defaulting to `Connector::Tcp` variant!");
1173 Connector::default()
1174 }
1175 };
1176
1177 #[cfg(not(feature = "tor"))]
1178 let connector = Connector::default();
1179
1180 let federation_id = invite_code.federation_id();
1181
1182 let mut federation_manager = self.federation_manager.write().await;
1183
1184 if federation_manager.has_federation(federation_id) {
1186 return Err(AdminGatewayError::ClientCreationError(anyhow!(
1187 "Federation has already been registered"
1188 )));
1189 }
1190
1191 let federation_index = federation_manager.pop_next_index()?;
1194
1195 let federation_config = FederationConfig {
1196 invite_code,
1197 federation_index,
1198 lightning_fee: self.default_routing_fees,
1199 transaction_fee: self.default_transaction_fees,
1200 connector,
1201 };
1202
1203 let recover = payload.recover.unwrap_or(false);
1204 if recover {
1205 self.client_builder
1206 .recover(
1207 federation_config.clone(),
1208 Arc::new(self.clone()),
1209 &self.mnemonic,
1210 )
1211 .await?;
1212 }
1213
1214 let client = self
1215 .client_builder
1216 .build(
1217 federation_config.clone(),
1218 Arc::new(self.clone()),
1219 &self.mnemonic,
1220 )
1221 .await?;
1222
1223 if recover {
1224 client.wait_for_all_active_state_machines().await?;
1225 }
1226
1227 let federation_info = FederationInfo {
1230 federation_id,
1231 federation_name: federation_manager.federation_name(&client).await,
1232 balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
1233 warn!(
1234 target: LOG_GATEWAY,
1235 err = %err.fmt_compact_anyhow(),
1236 %federation_id,
1237 "Balance not immediately available after joining/recovering."
1238 );
1239 Amount::default()
1240 }),
1241 config: federation_config.clone(),
1242 };
1243
1244 Self::check_lnv1_federation_network(&client, self.network).await?;
1245 Self::check_lnv2_federation_network(&client, self.network).await?;
1246 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1247 client
1248 .get_first_module::<GatewayClientModule>()?
1249 .try_register_with_federation(
1250 Vec::new(),
1252 GW_ANNOUNCEMENT_TTL,
1253 federation_config.lightning_fee.into(),
1254 lightning_context,
1255 self.versioned_api.clone(),
1256 self.gateway_id,
1257 )
1258 .await;
1259 }
1260
1261 federation_manager.add_client(
1263 federation_index,
1264 Spanned::new(
1265 info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
1266 async { client },
1267 )
1268 .await,
1269 );
1270
1271 let mut dbtx = self.gateway_db.begin_transaction().await;
1272 dbtx.save_federation_config(&federation_config).await;
1273 dbtx.commit_tx().await;
1274 debug!(
1275 target: LOG_GATEWAY,
1276 federation_id = %federation_id,
1277 federation_index = %federation_index,
1278 "Federation connected"
1279 );
1280
1281 Ok(federation_info)
1282 }
1283
1284 pub async fn handle_leave_federation(
1289 &self,
1290 payload: LeaveFedPayload,
1291 ) -> AdminResult<FederationInfo> {
1292 let mut federation_manager = self.federation_manager.write().await;
1295 let mut dbtx = self.gateway_db.begin_transaction().await;
1296
1297 let federation_info = federation_manager
1298 .leave_federation(payload.federation_id, &mut dbtx.to_ref_nc())
1299 .await?;
1300
1301 dbtx.remove_federation_config(payload.federation_id).await;
1302 dbtx.commit_tx().await;
1303 Ok(federation_info)
1304 }
1305
1306 pub async fn handle_backup_msg(
1309 &self,
1310 BackupPayload { federation_id }: BackupPayload,
1311 ) -> AdminResult<()> {
1312 let federation_manager = self.federation_manager.read().await;
1313 let client = federation_manager
1314 .client(&federation_id)
1315 .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1316 format!("Gateway has not connected to {federation_id}")
1317 )))?
1318 .value();
1319 let metadata: BTreeMap<String, String> = BTreeMap::new();
1320 client
1321 .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1322 metadata,
1323 ))
1324 .await?;
1325 Ok(())
1326 }
1327
1328 pub async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
1332 let words = self
1333 .mnemonic
1334 .words()
1335 .map(std::string::ToString::to_string)
1336 .collect::<Vec<_>>();
1337 let all_federations = self
1338 .federation_manager
1339 .read()
1340 .await
1341 .get_all_federation_configs()
1342 .await
1343 .keys()
1344 .copied()
1345 .collect::<BTreeSet<_>>();
1346 let legacy_federations = self.client_builder.legacy_federations(all_federations);
1347 let mnemonic_response = MnemonicResponse {
1348 mnemonic: words,
1349 legacy_federations,
1350 };
1351 Ok(mnemonic_response)
1352 }
1353
1354 pub async fn handle_set_fees_msg(
1357 &self,
1358 SetFeesPayload {
1359 federation_id,
1360 lightning_base,
1361 lightning_parts_per_million,
1362 transaction_base,
1363 transaction_parts_per_million,
1364 }: SetFeesPayload,
1365 ) -> AdminResult<()> {
1366 let mut dbtx = self.gateway_db.begin_transaction().await;
1367 let mut fed_configs = if let Some(fed_id) = federation_id {
1368 dbtx.load_federation_configs()
1369 .await
1370 .into_iter()
1371 .filter(|(id, _)| *id == fed_id)
1372 .collect::<BTreeMap<_, _>>()
1373 } else {
1374 dbtx.load_federation_configs().await
1375 };
1376
1377 let federation_manager = self.federation_manager.read().await;
1378
1379 for (federation_id, config) in &mut fed_configs {
1380 let mut lightning_fee = config.lightning_fee;
1381 if let Some(lightning_base) = lightning_base {
1382 lightning_fee.base = lightning_base;
1383 }
1384
1385 if let Some(lightning_ppm) = lightning_parts_per_million {
1386 lightning_fee.parts_per_million = lightning_ppm;
1387 }
1388
1389 let mut transaction_fee = config.transaction_fee;
1390 if let Some(transaction_base) = transaction_base {
1391 transaction_fee.base = transaction_base;
1392 }
1393
1394 if let Some(transaction_ppm) = transaction_parts_per_million {
1395 transaction_fee.parts_per_million = transaction_ppm;
1396 }
1397
1398 let client =
1399 federation_manager
1400 .client(federation_id)
1401 .ok_or(FederationNotConnected {
1402 federation_id_prefix: federation_id.to_prefix(),
1403 })?;
1404 let client_config = client.value().config().await;
1405 let contains_lnv2 = client_config
1406 .modules
1407 .values()
1408 .any(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
1409
1410 let send_fees = lightning_fee + transaction_fee;
1412 if contains_lnv2 && send_fees.gt(&PaymentFee::SEND_FEE_LIMIT) {
1413 return Err(AdminGatewayError::GatewayConfigurationError(format!(
1414 "Total Send fees exceeded {}",
1415 PaymentFee::SEND_FEE_LIMIT
1416 )));
1417 }
1418
1419 if contains_lnv2 && transaction_fee.gt(&PaymentFee::RECEIVE_FEE_LIMIT) {
1421 return Err(AdminGatewayError::GatewayConfigurationError(format!(
1422 "Transaction fees exceeded RECEIVE LIMIT {}",
1423 PaymentFee::RECEIVE_FEE_LIMIT
1424 )));
1425 }
1426
1427 config.lightning_fee = lightning_fee;
1428 config.transaction_fee = transaction_fee;
1429 dbtx.save_federation_config(config).await;
1430 }
1431
1432 dbtx.commit_tx().await;
1433
1434 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1435 let register_task_group = TaskGroup::new();
1436
1437 self.register_federations(&fed_configs, ®ister_task_group)
1438 .await;
1439 }
1440
1441 Ok(())
1442 }
1443
1444 pub async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
1446 let context = self.get_lightning_context().await?;
1447 let response = context.lnrpc.get_ln_onchain_address().await?;
1448
1449 let address = Address::from_str(&response.address).map_err(|e| {
1450 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1451 failure_reason: e.to_string(),
1452 })
1453 })?;
1454
1455 address.require_network(self.network).map_err(|e| {
1456 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1457 failure_reason: e.to_string(),
1458 })
1459 })
1460 }
1461
1462 pub async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
1465 info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
1466 let context = self.get_lightning_context().await?;
1467 let res = context.lnrpc.open_channel(payload).await?;
1468 info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
1469 Txid::from_str(&res.funding_txid).map_err(|e| {
1470 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1471 failure_reason: format!("Received invalid channel funding txid string {e}"),
1472 })
1473 })
1474 }
1475
1476 pub async fn handle_close_channels_with_peer_msg(
1479 &self,
1480 payload: CloseChannelsWithPeerRequest,
1481 ) -> AdminResult<CloseChannelsWithPeerResponse> {
1482 let context = self.get_lightning_context().await?;
1483 let response = context.lnrpc.close_channels_with_peer(payload).await?;
1484 Ok(response)
1485 }
1486
1487 pub async fn handle_list_channels_msg(
1490 &self,
1491 ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
1492 let context = self.get_lightning_context().await?;
1493 let response = context.lnrpc.list_channels().await?;
1494 Ok(response.channels)
1495 }
1496
1497 pub async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
1499 let context = self.get_lightning_context().await?;
1500 let response = context.lnrpc.send_onchain(payload).await?;
1501 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
1502 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
1503 })
1504 }
1505
1506 pub async fn handle_recheck_address_msg(
1508 &self,
1509 payload: DepositAddressRecheckPayload,
1510 ) -> AdminResult<()> {
1511 self.select_client(payload.federation_id)
1512 .await?
1513 .value()
1514 .get_first_module::<WalletClientModule>()
1515 .expect("Must have client module")
1516 .recheck_pegin_address_by_address(payload.address)
1517 .await?;
1518 Ok(())
1519 }
1520
1521 pub async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
1524 let dbtx = self.gateway_db.begin_transaction_nc().await;
1525 let federation_infos = self
1526 .federation_manager
1527 .read()
1528 .await
1529 .federation_info_all_federations(dbtx)
1530 .await;
1531
1532 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
1533 .iter()
1534 .map(|federation_info| FederationBalanceInfo {
1535 federation_id: federation_info.federation_id,
1536 ecash_balance_msats: Amount {
1537 msats: federation_info.balance_msat.msats,
1538 },
1539 })
1540 .collect();
1541
1542 let context = self.get_lightning_context().await?;
1543 let lightning_node_balances = context.lnrpc.get_balances().await?;
1544
1545 Ok(GatewayBalances {
1546 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
1547 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
1548 ecash_balances,
1549 inbound_lightning_liquidity_msats: lightning_node_balances
1550 .inbound_lightning_liquidity_msats,
1551 })
1552 }
1553
1554 pub async fn handle_spend_ecash_msg(
1556 &self,
1557 payload: SpendEcashPayload,
1558 ) -> AdminResult<SpendEcashResponse> {
1559 let client = self
1560 .select_client(payload.federation_id)
1561 .await?
1562 .into_value();
1563 let mint_module = client.get_first_module::<MintClientModule>()?;
1564 let timeout = Duration::from_secs(payload.timeout);
1565 let (operation_id, notes) = if payload.allow_overpay {
1566 let (operation_id, notes) = mint_module
1567 .spend_notes_with_selector(
1568 &SelectNotesWithAtleastAmount,
1569 payload.amount,
1570 timeout,
1571 payload.include_invite,
1572 (),
1573 )
1574 .await?;
1575
1576 let overspend_amount = notes.total_amount().saturating_sub(payload.amount);
1577 if overspend_amount != Amount::ZERO {
1578 warn!(
1579 target: LOG_GATEWAY,
1580 overspend_amount = %overspend_amount,
1581 "Selected notes worth more than requested",
1582 );
1583 }
1584
1585 (operation_id, notes)
1586 } else {
1587 mint_module
1588 .spend_notes_with_selector(
1589 &SelectNotesWithExactAmount,
1590 payload.amount,
1591 timeout,
1592 payload.include_invite,
1593 (),
1594 )
1595 .await?
1596 };
1597
1598 debug!(target: LOG_GATEWAY, ?operation_id, ?notes, "Spend ecash notes");
1599
1600 Ok(SpendEcashResponse {
1601 operation_id,
1602 notes,
1603 })
1604 }
1605
1606 pub async fn handle_receive_ecash_msg(
1608 &self,
1609 payload: ReceiveEcashPayload,
1610 ) -> Result<ReceiveEcashResponse> {
1611 let amount = payload.notes.total_amount();
1612 let client = self
1613 .federation_manager
1614 .read()
1615 .await
1616 .get_client_for_federation_id_prefix(payload.notes.federation_id_prefix())
1617 .ok_or(FederationNotConnected {
1618 federation_id_prefix: payload.notes.federation_id_prefix(),
1619 })?;
1620 let mint = client
1621 .value()
1622 .get_first_module::<MintClientModule>()
1623 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1624 failure_reason: format!("Mint module does not exist: {e:?}"),
1625 })?;
1626
1627 let operation_id = mint
1628 .reissue_external_notes(payload.notes, ())
1629 .await
1630 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1631 failure_reason: e.to_string(),
1632 })?;
1633 if payload.wait {
1634 let mut updates = mint
1635 .subscribe_reissue_external_notes(operation_id)
1636 .await
1637 .unwrap()
1638 .into_stream();
1639
1640 while let Some(update) = updates.next().await {
1641 if let fedimint_mint_client::ReissueExternalNotesState::Failed(e) = update {
1642 return Err(PublicGatewayError::ReceiveEcashError {
1643 failure_reason: e.to_string(),
1644 });
1645 }
1646 }
1647 }
1648
1649 Ok(ReceiveEcashResponse { amount })
1650 }
1651
1652 pub async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
1655 let mut state_guard = self.state.write().await;
1657 if let GatewayState::Running { lightning_context } = state_guard.clone() {
1658 *state_guard = GatewayState::ShuttingDown { lightning_context };
1659
1660 self.federation_manager
1661 .read()
1662 .await
1663 .wait_for_incoming_payments()
1664 .await?;
1665 }
1666
1667 let tg = task_group.clone();
1668 tg.spawn("Kill Gateway", |_task_handle| async {
1669 if let Err(err) = task_group.shutdown_join_all(Duration::from_secs(180)).await {
1670 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error shutting down gateway");
1671 }
1672 });
1673 Ok(())
1674 }
1675
1676 pub async fn handle_payment_log_msg(
1678 &self,
1679 PaymentLogPayload {
1680 end_position,
1681 pagination_size,
1682 federation_id,
1683 event_kinds,
1684 }: PaymentLogPayload,
1685 ) -> AdminResult<PaymentLogResponse> {
1686 const BATCH_SIZE: u64 = 10_000;
1687 let federation_manager = self.federation_manager.read().await;
1688 let client = federation_manager
1689 .client(&federation_id)
1690 .ok_or(FederationNotConnected {
1691 federation_id_prefix: federation_id.to_prefix(),
1692 })?
1693 .value();
1694
1695 let event_kinds = if event_kinds.is_empty() {
1696 ALL_GATEWAY_EVENTS.to_vec()
1697 } else {
1698 event_kinds
1699 };
1700
1701 let end_position = if let Some(position) = end_position {
1702 position
1703 } else {
1704 let mut dbtx = client.db().begin_transaction_nc().await;
1705 dbtx.get_next_event_log_id().await
1706 };
1707
1708 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
1709
1710 let mut payment_log = Vec::new();
1711
1712 while payment_log.len() < pagination_size {
1713 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
1714 let mut filtered_batch = batch
1715 .into_iter()
1716 .filter(|e| e.event_id <= end_position && event_kinds.contains(&e.event_kind))
1717 .collect::<Vec<_>>();
1718 filtered_batch.reverse();
1719 payment_log.extend(filtered_batch);
1720
1721 start_position = start_position.saturating_sub(BATCH_SIZE);
1723
1724 if start_position == EventLogId::LOG_START {
1725 break;
1726 }
1727 }
1728
1729 payment_log.truncate(pagination_size);
1731
1732 Ok(PaymentLogResponse(payment_log))
1733 }
1734
1735 pub async fn handle_payment_summary_msg(
1738 &self,
1739 PaymentSummaryPayload {
1740 start_millis,
1741 end_millis,
1742 }: PaymentSummaryPayload,
1743 ) -> AdminResult<PaymentSummaryResponse> {
1744 let federation_manager = self.federation_manager.read().await;
1745 let fed_configs = federation_manager.get_all_federation_configs().await;
1746 let federation_ids = fed_configs.keys().collect::<Vec<_>>();
1747 let start = UNIX_EPOCH + Duration::from_millis(start_millis);
1748 let end = UNIX_EPOCH + Duration::from_millis(end_millis);
1749
1750 if start > end {
1751 return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
1752 }
1753
1754 let mut outgoing = StructuredPaymentEvents::default();
1755 let mut incoming = StructuredPaymentEvents::default();
1756 for fed_id in federation_ids {
1757 let client = federation_manager
1758 .client(fed_id)
1759 .expect("No client available")
1760 .value();
1761 let all_events = &get_events_for_duration(client, start, end).await;
1762
1763 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
1764 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
1765 outgoing.combine(&mut lnv1_outgoing);
1766 incoming.combine(&mut lnv1_incoming);
1767 outgoing.combine(&mut lnv2_outgoing);
1768 incoming.combine(&mut lnv2_incoming);
1769 }
1770
1771 Ok(PaymentSummaryResponse {
1772 outgoing: PaymentStats::compute(&outgoing),
1773 incoming: PaymentStats::compute(&incoming),
1774 })
1775 }
1776
1777 pub async fn handle_get_invoice_msg(
1780 &self,
1781 payload: GetInvoiceRequest,
1782 ) -> AdminResult<Option<GetInvoiceResponse>> {
1783 let lightning_context = self.get_lightning_context().await?;
1784 let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1785 Ok(invoice)
1786 }
1787
1788 pub async fn handle_list_transactions_msg(
1789 &self,
1790 payload: ListTransactionsPayload,
1791 ) -> AdminResult<ListTransactionsResponse> {
1792 let lightning_context = self.get_lightning_context().await?;
1793 let response = lightning_context
1794 .lnrpc
1795 .list_transactions(payload.start_secs, payload.end_secs)
1796 .await?;
1797 Ok(response)
1798 }
1799
1800 pub async fn handle_create_offer_for_operator_msg(
1802 &self,
1803 payload: CreateOfferPayload,
1804 ) -> AdminResult<CreateOfferResponse> {
1805 let lightning_context = self.get_lightning_context().await?;
1806 let offer = lightning_context.lnrpc.create_offer(
1807 payload.amount,
1808 payload.description,
1809 payload.expiry_secs,
1810 payload.quantity,
1811 )?;
1812 Ok(CreateOfferResponse { offer })
1813 }
1814
1815 pub async fn handle_pay_offer_for_operator_msg(
1817 &self,
1818 payload: PayOfferPayload,
1819 ) -> AdminResult<PayOfferResponse> {
1820 let lightning_context = self.get_lightning_context().await?;
1821 let preimage = lightning_context
1822 .lnrpc
1823 .pay_offer(
1824 payload.offer,
1825 payload.quantity,
1826 payload.amount,
1827 payload.payer_note,
1828 )
1829 .await?;
1830 Ok(PayOfferResponse {
1831 preimage: preimage.to_string(),
1832 })
1833 }
1834
1835 async fn register_federations(
1837 &self,
1838 federations: &BTreeMap<FederationId, FederationConfig>,
1839 register_task_group: &TaskGroup,
1840 ) {
1841 if let Ok(lightning_context) = self.get_lightning_context().await {
1842 let route_hints = lightning_context
1843 .lnrpc
1844 .parsed_route_hints(self.num_route_hints)
1845 .await;
1846 if route_hints.is_empty() {
1847 warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
1848 }
1849
1850 for (federation_id, federation_config) in federations {
1851 let fed_manager = self.federation_manager.read().await;
1852 if let Some(client) = fed_manager.client(federation_id) {
1853 let client_arc = client.clone().into_value();
1854 let route_hints = route_hints.clone();
1855 let lightning_context = lightning_context.clone();
1856 let federation_config = federation_config.clone();
1857 let api = self.versioned_api.clone();
1858 let gateway_id = self.gateway_id;
1859
1860 if let Err(err) = register_task_group
1861 .spawn_cancellable("register_federation", async move {
1862 let gateway_client = client_arc
1863 .get_first_module::<GatewayClientModule>()
1864 .expect("No GatewayClientModule exists");
1865 gateway_client
1866 .try_register_with_federation(
1867 route_hints,
1868 GW_ANNOUNCEMENT_TTL,
1869 federation_config.lightning_fee.into(),
1870 lightning_context,
1871 api,
1872 gateway_id,
1873 )
1874 .await;
1875 })
1876 .await
1877 {
1878 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to shutdown register federation task");
1879 }
1880 }
1881 }
1882 }
1883 }
1884
1885 pub async fn select_client(
1888 &self,
1889 federation_id: FederationId,
1890 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
1891 {
1892 self.federation_manager
1893 .read()
1894 .await
1895 .client(&federation_id)
1896 .cloned()
1897 .ok_or(FederationNotConnected {
1898 federation_id_prefix: federation_id.to_prefix(),
1899 })
1900 }
1901
1902 async fn load_or_generate_mnemonic(gateway_db: &Database) -> AdminResult<Mnemonic> {
1907 Ok(
1908 if let Ok(entropy) = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db).await {
1909 Mnemonic::from_entropy(&entropy)
1910 .map_err(|e| AdminGatewayError::MnemonicError(anyhow!(e.to_string())))?
1911 } else {
1912 let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
1913 info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
1914 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
1915 |e| {
1916 AdminGatewayError::MnemonicError(anyhow!(format!(
1917 "Seed phrase provided in environment was invalid {e:?}"
1918 )))
1919 },
1920 )?
1921 } else {
1922 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
1923 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
1924 };
1925
1926 Client::store_encodable_client_secret(gateway_db, mnemonic.to_entropy())
1927 .await
1928 .map_err(AdminGatewayError::MnemonicError)?;
1929 mnemonic
1930 },
1931 )
1932 }
1933
1934 async fn load_clients(&self) -> AdminResult<()> {
1938 let mut federation_manager = self.federation_manager.write().await;
1939
1940 let configs = {
1941 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1942 dbtx.load_federation_configs().await
1943 };
1944
1945 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
1946 federation_manager.set_next_index(max_federation_index + 1);
1947 }
1948
1949 for (federation_id, config) in configs {
1950 let federation_index = config.federation_index;
1951 match Box::pin(Spanned::try_new(
1952 info_span!(target: LOG_GATEWAY, "client", federation_id = %federation_id.clone()),
1953 self.client_builder
1954 .build(config, Arc::new(self.clone()), &self.mnemonic),
1955 ))
1956 .await
1957 {
1958 Ok(client) => {
1959 federation_manager.add_client(federation_index, client);
1960 }
1961 _ => {
1962 warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
1963 }
1964 }
1965 }
1966
1967 Ok(())
1968 }
1969
1970 fn register_clients_timer(&self) {
1976 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1978 info!(target: LOG_GATEWAY, "Spawning register task...");
1979 let gateway = self.clone();
1980 let register_task_group = self.task_group.make_subgroup();
1981 self.task_group.spawn_cancellable("register clients", async move {
1982 loop {
1983 let gateway_state = gateway.get_state().await;
1984 if let GatewayState::Running { .. } = &gateway_state {
1985 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
1986 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
1987 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
1988 } else {
1989 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
1991 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");
1992 sleep(NOT_RUNNING_RETRY).await;
1993 continue;
1994 }
1995
1996 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
1999 }
2000 });
2001 }
2002 }
2003
2004 async fn check_lnv1_federation_network(
2007 client: &ClientHandleArc,
2008 network: Network,
2009 ) -> AdminResult<()> {
2010 let federation_id = client.federation_id();
2011 let config = client.config().await;
2012 let cfg = config
2013 .modules
2014 .values()
2015 .find(|m| LightningCommonInit::KIND == m.kind)
2016 .ok_or(AdminGatewayError::ClientCreationError(anyhow!(format!(
2017 "Federation {federation_id} does not have an LNv1 module"
2018 ))))?;
2019 let ln_cfg: &LightningClientConfig = cfg.cast()?;
2020
2021 if ln_cfg.network.0 != network {
2022 crit!(
2023 target: LOG_GATEWAY,
2024 federation_id = %federation_id,
2025 network = %network,
2026 "Incorrect network for federation",
2027 );
2028 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2029 "Unsupported network {}",
2030 ln_cfg.network
2031 ))));
2032 }
2033
2034 Ok(())
2035 }
2036
2037 async fn check_lnv2_federation_network(
2040 client: &ClientHandleArc,
2041 network: Network,
2042 ) -> AdminResult<()> {
2043 let federation_id = client.federation_id();
2044 let config = client.config().await;
2045 let cfg = config
2046 .modules
2047 .values()
2048 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
2049
2050 if let Some(cfg) = cfg {
2052 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig = cfg.cast()?;
2053
2054 if ln_cfg.network != network {
2055 crit!(
2056 target: LOG_GATEWAY,
2057 federation_id = %federation_id,
2058 network = %network,
2059 "Incorrect network for federation",
2060 );
2061 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2062 "Unsupported network {}",
2063 ln_cfg.network
2064 ))));
2065 }
2066 }
2067
2068 Ok(())
2069 }
2070
2071 pub async fn get_lightning_context(
2075 &self,
2076 ) -> std::result::Result<LightningContext, LightningRpcError> {
2077 match self.get_state().await {
2078 GatewayState::Running { lightning_context }
2079 | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
2080 _ => Err(LightningRpcError::FailedToConnect),
2081 }
2082 }
2083
2084 pub async fn unannounce_from_all_federations(&self) {
2087 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2088 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2089 let gateway_keypair = dbtx.load_gateway_keypair_assert_exists().await;
2090
2091 self.federation_manager
2092 .read()
2093 .await
2094 .unannounce_from_all_federations(gateway_keypair)
2095 .await;
2096 }
2097 }
2098
2099 fn create_lightning_client(
2100 &self,
2101 runtime: Arc<tokio::runtime::Runtime>,
2102 ) -> Box<dyn ILnRpcClient> {
2103 match self.lightning_mode.clone() {
2104 LightningMode::Lnd {
2105 lnd_rpc_addr,
2106 lnd_tls_cert,
2107 lnd_macaroon,
2108 } => Box::new(GatewayLndClient::new(
2109 lnd_rpc_addr,
2110 lnd_tls_cert,
2111 lnd_macaroon,
2112 None,
2113 )),
2114 LightningMode::Ldk {
2115 lightning_port,
2116 alias,
2117 } => Box::new(
2118 ldk::GatewayLdkClient::new(
2119 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
2120 self.chain_source.clone(),
2121 self.network,
2122 lightning_port,
2123 alias,
2124 self.mnemonic.clone(),
2125 runtime,
2126 )
2127 .expect("Failed to create LDK client"),
2128 ),
2129 }
2130 }
2131}
2132
2133impl Gateway {
2135 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
2139 self.federation_manager
2140 .read()
2141 .await
2142 .client(federation_id)
2143 .map(|client| {
2144 client
2145 .value()
2146 .get_first_module::<GatewayClientModuleV2>()
2147 .expect("Must have client module")
2148 .keypair
2149 .public_key()
2150 })
2151 }
2152
2153 pub async fn routing_info_v2(
2156 &self,
2157 federation_id: &FederationId,
2158 ) -> Result<Option<RoutingInfo>> {
2159 let context = self.get_lightning_context().await?;
2160
2161 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2162 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
2163 PublicGatewayError::FederationNotConnected(FederationNotConnected {
2164 federation_id_prefix: federation_id.to_prefix(),
2165 }),
2166 )?;
2167
2168 let lightning_fee = fed_config.lightning_fee;
2169 let transaction_fee = fed_config.transaction_fee;
2170
2171 Ok(self
2172 .public_key_v2(federation_id)
2173 .await
2174 .map(|module_public_key| RoutingInfo {
2175 lightning_public_key: context.lightning_public_key,
2176 module_public_key,
2177 send_fee_default: lightning_fee + transaction_fee,
2178 send_fee_minimum: transaction_fee,
2182 expiration_delta_default: 1440,
2183 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
2184 receive_fee: transaction_fee,
2187 }))
2188 }
2189
2190 async fn send_payment_v2(
2193 &self,
2194 payload: SendPaymentPayload,
2195 ) -> Result<std::result::Result<[u8; 32], Signature>> {
2196 self.select_client(payload.federation_id)
2197 .await?
2198 .value()
2199 .get_first_module::<GatewayClientModuleV2>()
2200 .expect("Must have client module")
2201 .send_payment(payload)
2202 .await
2203 .map_err(LNv2Error::OutgoingPayment)
2204 .map_err(PublicGatewayError::LNv2)
2205 }
2206
2207 async fn create_bolt11_invoice_v2(
2212 &self,
2213 payload: CreateBolt11InvoicePayload,
2214 ) -> Result<Bolt11Invoice> {
2215 if !payload.contract.verify() {
2216 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2217 "The contract is invalid".to_string(),
2218 )));
2219 }
2220
2221 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
2222 LNv2Error::IncomingPayment(format!(
2223 "Federation {} does not exist",
2224 payload.federation_id
2225 )),
2226 )?;
2227
2228 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
2229 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2230 "The incoming contract is keyed to another gateway".to_string(),
2231 )));
2232 }
2233
2234 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
2235
2236 if contract_amount == Amount::ZERO {
2237 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2238 "Zero amount incoming contracts are not supported".to_string(),
2239 )));
2240 }
2241
2242 if contract_amount != payload.contract.commitment.amount {
2243 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2244 "The contract amount does not pay the correct amount of fees".to_string(),
2245 )));
2246 }
2247
2248 if payload.contract.commitment.expiration <= duration_since_epoch().as_secs() {
2249 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2250 "The contract has already expired".to_string(),
2251 )));
2252 }
2253
2254 let payment_hash = match payload.contract.commitment.payment_image {
2255 PaymentImage::Hash(payment_hash) => payment_hash,
2256 PaymentImage::Point(..) => {
2257 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2258 "PaymentImage is not a payment hash".to_string(),
2259 )));
2260 }
2261 };
2262
2263 let invoice = self
2264 .create_invoice_via_lnrpc_v2(
2265 payment_hash,
2266 payload.amount,
2267 payload.description.clone(),
2268 payload.expiry_secs,
2269 )
2270 .await?;
2271
2272 let mut dbtx = self.gateway_db.begin_transaction().await;
2273
2274 if dbtx
2275 .save_registered_incoming_contract(
2276 payload.federation_id,
2277 payload.amount,
2278 payload.contract,
2279 )
2280 .await
2281 .is_some()
2282 {
2283 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2284 "PaymentHash is already registered".to_string(),
2285 )));
2286 }
2287
2288 dbtx.commit_tx_result().await.map_err(|_| {
2289 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2290 "Payment hash is already registered".to_string(),
2291 ))
2292 })?;
2293
2294 Ok(invoice)
2295 }
2296
2297 pub async fn create_invoice_via_lnrpc_v2(
2300 &self,
2301 payment_hash: sha256::Hash,
2302 amount: Amount,
2303 description: Bolt11InvoiceDescription,
2304 expiry_time: u32,
2305 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
2306 let lnrpc = self.get_lightning_context().await?.lnrpc;
2307
2308 let response = match description {
2309 Bolt11InvoiceDescription::Direct(description) => {
2310 lnrpc
2311 .create_invoice(CreateInvoiceRequest {
2312 payment_hash: Some(payment_hash),
2313 amount_msat: amount.msats,
2314 expiry_secs: expiry_time,
2315 description: Some(InvoiceDescription::Direct(description)),
2316 })
2317 .await?
2318 }
2319 Bolt11InvoiceDescription::Hash(hash) => {
2320 lnrpc
2321 .create_invoice(CreateInvoiceRequest {
2322 payment_hash: Some(payment_hash),
2323 amount_msat: amount.msats,
2324 expiry_secs: expiry_time,
2325 description: Some(InvoiceDescription::Hash(hash)),
2326 })
2327 .await?
2328 }
2329 };
2330
2331 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
2332 LightningRpcError::FailedToGetInvoice {
2333 failure_reason: e.to_string(),
2334 }
2335 })
2336 }
2337
2338 pub async fn verify_bolt11_preimage_v2(
2339 &self,
2340 payment_hash: sha256::Hash,
2341 wait: bool,
2342 ) -> std::result::Result<VerifyResponse, String> {
2343 let registered_contract = self
2344 .gateway_db
2345 .begin_transaction_nc()
2346 .await
2347 .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
2348 .await
2349 .ok_or("Unknown payment hash".to_string())?;
2350
2351 let client = self
2352 .select_client(registered_contract.federation_id)
2353 .await
2354 .map_err(|_| "Not connected to federation".to_string())?
2355 .into_value();
2356
2357 let operation_id = OperationId::from_encodable(®istered_contract.contract);
2358
2359 if !(wait || client.operation_exists(operation_id).await) {
2360 return Ok(VerifyResponse {
2361 status: "OK".to_string(),
2362 settled: false,
2363 preimage: None,
2364 });
2365 }
2366
2367 let state = client
2368 .get_first_module::<GatewayClientModuleV2>()
2369 .expect("Must have client module")
2370 .await_receive(operation_id)
2371 .await;
2372
2373 let preimage = match state {
2374 FinalReceiveState::Success(preimage) => Ok(preimage),
2375 FinalReceiveState::Failure => Err("Payment has failed".to_string()),
2376 FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
2377 FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
2378 }?;
2379
2380 Ok(VerifyResponse {
2381 status: "OK".to_string(),
2382 settled: true,
2383 preimage: Some(preimage),
2384 })
2385 }
2386
2387 pub async fn get_registered_incoming_contract_and_client_v2(
2391 &self,
2392 payment_image: PaymentImage,
2393 amount_msats: u64,
2394 ) -> Result<(IncomingContract, ClientHandleArc)> {
2395 let registered_incoming_contract = self
2396 .gateway_db
2397 .begin_transaction_nc()
2398 .await
2399 .load_registered_incoming_contract(payment_image)
2400 .await
2401 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2402 "No corresponding decryption contract available".to_string(),
2403 )))?;
2404
2405 if registered_incoming_contract.incoming_amount_msats != amount_msats {
2406 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
2407 "The available decryption contract's amount is not equal to the requested amount"
2408 .to_string(),
2409 )));
2410 }
2411
2412 let client = self
2413 .select_client(registered_incoming_contract.federation_id)
2414 .await?
2415 .into_value();
2416
2417 Ok((registered_incoming_contract.contract, client))
2418 }
2419}
2420
2421#[async_trait]
2422impl IGatewayClientV2 for Gateway {
2423 async fn complete_htlc(&self, htlc_response: InterceptPaymentResponse) {
2424 loop {
2425 match self.get_lightning_context().await {
2426 Ok(lightning_context) => {
2427 match lightning_context
2428 .lnrpc
2429 .complete_htlc(htlc_response.clone())
2430 .await
2431 {
2432 Ok(..) => return,
2433 Err(err) => {
2434 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2435 }
2436 }
2437 }
2438 Err(err) => {
2439 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2440 }
2441 }
2442
2443 sleep(Duration::from_secs(5)).await;
2444 }
2445 }
2446
2447 async fn is_direct_swap(
2448 &self,
2449 invoice: &Bolt11Invoice,
2450 ) -> anyhow::Result<Option<(IncomingContract, ClientHandleArc)>> {
2451 let lightning_context = self.get_lightning_context().await?;
2452 if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
2453 let (contract, client) = self
2454 .get_registered_incoming_contract_and_client_v2(
2455 PaymentImage::Hash(*invoice.payment_hash()),
2456 invoice
2457 .amount_milli_satoshis()
2458 .expect("The amount invoice has been previously checked"),
2459 )
2460 .await?;
2461 Ok(Some((contract, client)))
2462 } else {
2463 Ok(None)
2464 }
2465 }
2466
2467 async fn pay(
2468 &self,
2469 invoice: Bolt11Invoice,
2470 max_delay: u64,
2471 max_fee: Amount,
2472 ) -> std::result::Result<[u8; 32], LightningRpcError> {
2473 let lightning_context = self.get_lightning_context().await?;
2474 lightning_context
2475 .lnrpc
2476 .pay(invoice, max_delay, max_fee)
2477 .await
2478 .map(|response| response.preimage.0)
2479 }
2480
2481 async fn min_contract_amount(
2482 &self,
2483 federation_id: &FederationId,
2484 amount: u64,
2485 ) -> anyhow::Result<Amount> {
2486 Ok(self
2487 .routing_info_v2(federation_id)
2488 .await?
2489 .ok_or(anyhow!("Routing Info not available"))?
2490 .send_fee_minimum
2491 .add_to(amount))
2492 }
2493
2494 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
2495 let rhints = invoice.route_hints();
2496 match rhints.first().and_then(|rh| rh.0.last()) {
2497 None => None,
2498 Some(hop) => match self.get_lightning_context().await {
2499 Ok(lightning_context) => {
2500 if hop.src_node_id != lightning_context.lightning_public_key {
2501 return None;
2502 }
2503
2504 self.federation_manager
2505 .read()
2506 .await
2507 .get_client_for_index(hop.short_channel_id)
2508 }
2509 Err(_) => None,
2510 },
2511 }
2512 }
2513
2514 async fn relay_lnv1_swap(
2515 &self,
2516 client: &ClientHandleArc,
2517 invoice: &Bolt11Invoice,
2518 ) -> anyhow::Result<FinalReceiveState> {
2519 let swap_params = SwapParameters {
2520 payment_hash: *invoice.payment_hash(),
2521 amount_msat: Amount::from_msats(
2522 invoice
2523 .amount_milli_satoshis()
2524 .ok_or(anyhow!("Amountless invoice not supported"))?,
2525 ),
2526 };
2527 let lnv1 = client
2528 .get_first_module::<GatewayClientModule>()
2529 .expect("No LNv1 module");
2530 let operation_id = lnv1.gateway_handle_direct_swap(swap_params).await?;
2531 let mut stream = lnv1
2532 .gateway_subscribe_ln_receive(operation_id)
2533 .await?
2534 .into_stream();
2535 let mut final_state = FinalReceiveState::Failure;
2536 while let Some(update) = stream.next().await {
2537 match update {
2538 GatewayExtReceiveStates::Funding => {}
2539 GatewayExtReceiveStates::FundingFailed { error: _ } => {
2540 final_state = FinalReceiveState::Rejected;
2541 }
2542 GatewayExtReceiveStates::Preimage(preimage) => {
2543 final_state = FinalReceiveState::Success(preimage.0);
2544 }
2545 GatewayExtReceiveStates::RefundError {
2546 error_message: _,
2547 error: _,
2548 } => {
2549 final_state = FinalReceiveState::Failure;
2550 }
2551 GatewayExtReceiveStates::RefundSuccess {
2552 out_points: _,
2553 error: _,
2554 } => {
2555 final_state = FinalReceiveState::Refunded;
2556 }
2557 }
2558 }
2559
2560 Ok(final_state)
2561 }
2562}
2563
2564#[async_trait]
2565impl IGatewayClientV1 for Gateway {
2566 async fn verify_preimage_authentication(
2567 &self,
2568 payment_hash: sha256::Hash,
2569 preimage_auth: sha256::Hash,
2570 contract: OutgoingContractAccount,
2571 ) -> std::result::Result<(), OutgoingPaymentError> {
2572 let mut dbtx = self.gateway_db.begin_transaction().await;
2573 if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
2574 if secret_hash != preimage_auth {
2575 return Err(OutgoingPaymentError {
2576 error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
2577 contract_id: contract.contract.contract_id(),
2578 contract: Some(contract),
2579 });
2580 }
2581 } else {
2582 dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
2585 .await;
2586 return dbtx
2587 .commit_tx_result()
2588 .await
2589 .map_err(|_| OutgoingPaymentError {
2590 error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
2591 contract_id: contract.contract.contract_id(),
2592 contract: Some(contract),
2593 });
2594 }
2595
2596 Ok(())
2597 }
2598
2599 async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()> {
2600 let lightning_context = self.get_lightning_context().await?;
2601
2602 if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
2603 ensure!(
2604 lightning_context.lnrpc.supports_private_payments(),
2605 "Private payments are not supported by the lightning node"
2606 );
2607 }
2608
2609 Ok(())
2610 }
2611
2612 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
2613 let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
2614 gateway_dbtx
2615 .load_federation_config(federation_id)
2616 .await
2617 .map(|c| c.lightning_fee.into())
2618 }
2619
2620 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
2621 self.federation_manager
2622 .read()
2623 .await
2624 .client(federation_id)
2625 .cloned()
2626 }
2627
2628 async fn get_client_for_invoice(
2629 &self,
2630 payment_data: PaymentData,
2631 ) -> Option<Spanned<ClientHandleArc>> {
2632 let rhints = payment_data.route_hints();
2633 match rhints.first().and_then(|rh| rh.0.last()) {
2634 None => None,
2635 Some(hop) => match self.get_lightning_context().await {
2636 Ok(lightning_context) => {
2637 if hop.src_node_id != lightning_context.lightning_public_key {
2638 return None;
2639 }
2640
2641 self.federation_manager
2642 .read()
2643 .await
2644 .get_client_for_index(hop.short_channel_id)
2645 }
2646 Err(_) => None,
2647 },
2648 }
2649 }
2650
2651 async fn pay(
2652 &self,
2653 payment_data: PaymentData,
2654 max_delay: u64,
2655 max_fee: Amount,
2656 ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
2657 let lightning_context = self.get_lightning_context().await?;
2658
2659 match payment_data {
2660 PaymentData::Invoice(invoice) => {
2661 lightning_context
2662 .lnrpc
2663 .pay(invoice, max_delay, max_fee)
2664 .await
2665 }
2666 PaymentData::PrunedInvoice(invoice) => {
2667 lightning_context
2668 .lnrpc
2669 .pay_private(invoice, max_delay, max_fee)
2670 .await
2671 }
2672 }
2673 }
2674
2675 async fn complete_htlc(
2676 &self,
2677 htlc: InterceptPaymentResponse,
2678 ) -> std::result::Result<(), LightningRpcError> {
2679 let lightning_context = loop {
2681 match self.get_lightning_context().await {
2682 Ok(lightning_context) => break lightning_context,
2683 Err(err) => {
2684 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failure trying to complete payment");
2685 sleep(Duration::from_secs(5)).await;
2686 }
2687 }
2688 };
2689
2690 lightning_context.lnrpc.complete_htlc(htlc).await
2691 }
2692
2693 async fn is_lnv2_direct_swap(
2694 &self,
2695 payment_hash: sha256::Hash,
2696 amount: Amount,
2697 ) -> anyhow::Result<
2698 Option<(
2699 fedimint_lnv2_common::contracts::IncomingContract,
2700 ClientHandleArc,
2701 )>,
2702 > {
2703 let (contract, client) = self
2704 .get_registered_incoming_contract_and_client_v2(
2705 PaymentImage::Hash(payment_hash),
2706 amount.msats,
2707 )
2708 .await?;
2709 Ok(Some((contract, client)))
2710 }
2711}