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 = GatewayClientBuilder::new(
375 opts.data_dir.clone(),
376 registry,
377 fedimint_mint_client::KIND,
378 opts.db_backend,
379 );
380
381 info!(
382 target: LOG_GATEWAY,
383 version = %fedimint_build_code_version_env!(),
384 "Starting gatewayd",
385 );
386
387 Gateway::new(
388 opts.mode,
389 gateway_parameters,
390 gateway_db,
391 client_builder,
392 GatewayState::Disconnected,
393 chain_source,
394 )
395 .await
396 }
397
398 async fn new(
401 lightning_mode: LightningMode,
402 gateway_parameters: GatewayParameters,
403 gateway_db: Database,
404 client_builder: GatewayClientBuilder,
405 gateway_state: GatewayState,
406 chain_source: ChainSource,
407 ) -> anyhow::Result<Gateway> {
408 apply_migrations(
411 &gateway_db,
412 (),
413 "gatewayd".to_string(),
414 get_gatewayd_database_migrations(),
415 None,
416 None,
417 )
418 .await?;
419
420 let num_route_hints = gateway_parameters.num_route_hints;
421 let network = gateway_parameters.network;
422
423 let task_group = TaskGroup::new();
424 task_group.install_kill_handler();
425
426 Ok(Self {
427 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
428 mnemonic: Self::load_or_generate_mnemonic(&gateway_db).await?,
429 lightning_mode,
430 state: Arc::new(RwLock::new(gateway_state)),
431 client_builder,
432 gateway_id: Self::load_or_create_gateway_id(&gateway_db).await,
433 gateway_db,
434 versioned_api: gateway_parameters.versioned_api,
435 listen: gateway_parameters.listen,
436 task_group,
437 bcrypt_password_hash: Arc::new(gateway_parameters.bcrypt_password_hash),
438 num_route_hints,
439 network,
440 chain_source,
441 default_routing_fees: gateway_parameters.default_routing_fees,
442 default_transaction_fees: gateway_parameters.default_transaction_fees,
443 })
444 }
445
446 async fn load_or_create_gateway_id(gateway_db: &Database) -> PublicKey {
448 let mut dbtx = gateway_db.begin_transaction().await;
449 let keypair = dbtx.load_or_create_gateway_keypair().await;
450 dbtx.commit_tx().await;
451 keypair.public_key()
452 }
453
454 pub fn gateway_id(&self) -> PublicKey {
455 self.gateway_id
456 }
457
458 pub fn versioned_api(&self) -> &SafeUrl {
459 &self.versioned_api
460 }
461
462 async fn get_state(&self) -> GatewayState {
463 self.state.read().await.clone()
464 }
465
466 pub async fn dump_database(
469 dbtx: &mut DatabaseTransaction<'_>,
470 prefix_names: Vec<String>,
471 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
472 dbtx.dump_database(prefix_names).await
473 }
474
475 pub async fn run(
480 self,
481 runtime: Arc<tokio::runtime::Runtime>,
482 ) -> anyhow::Result<TaskShutdownToken> {
483 install_crypto_provider().await;
484 self.register_clients_timer();
485 self.load_clients().await?;
486 self.start_gateway(runtime);
487 let handle = self.task_group.make_handle();
489 run_webserver(Arc::new(self)).await?;
490 let shutdown_receiver = handle.make_shutdown_rx();
491 Ok(shutdown_receiver)
492 }
493
494 fn start_gateway(&self, runtime: Arc<tokio::runtime::Runtime>) {
497 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 5;
498
499 let self_copy = self.clone();
500 let tg = self.task_group.clone();
501 self.task_group.spawn(
502 "Subscribe to intercepted lightning payments in stream",
503 |handle| async move {
504 loop {
506 if handle.is_shutting_down() {
507 info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
508 break;
509 }
510
511 let payment_stream_task_group = tg.make_subgroup();
512 let lnrpc_route = self_copy.create_lightning_client(runtime.clone());
513
514 debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
515 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
516 {
517 Ok((stream, ln_client)) => (stream, ln_client),
518 Err(err) => {
519 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
520 continue
521 }
522 };
523
524 self_copy.set_gateway_state(GatewayState::Connected).await;
526 info!(target: LOG_GATEWAY, "Established lightning payment stream");
527
528 let route_payments_response =
529 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
530
531 self_copy.set_gateway_state(GatewayState::Disconnected).await;
532 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
533 crit!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Lightning payment stream task group shutdown");
534 }
535
536 self_copy.unannounce_from_all_federations().await;
537
538 match route_payments_response {
539 ReceivePaymentStreamAction::RetryAfterDelay => {
540 warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
541 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
542 }
543 ReceivePaymentStreamAction::NoRetry => break,
544 }
545 }
546 },
547 );
548 }
549
550 async fn route_lightning_payments<'a>(
554 &'a self,
555 handle: &TaskHandle,
556 mut stream: RouteHtlcStream<'a>,
557 ln_client: Arc<dyn ILnRpcClient>,
558 ) -> ReceivePaymentStreamAction {
559 let (lightning_public_key, lightning_alias, lightning_network, synced_to_chain) =
560 match ln_client.parsed_node_info().await {
561 Ok((
562 lightning_public_key,
563 lightning_alias,
564 lightning_network,
565 _block_height,
566 synced_to_chain,
567 )) => (
568 lightning_public_key,
569 lightning_alias,
570 lightning_network,
571 synced_to_chain,
572 ),
573 Err(err) => {
574 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to retrieve Lightning info");
575 return ReceivePaymentStreamAction::RetryAfterDelay;
576 }
577 };
578
579 assert!(
580 self.network == lightning_network,
581 "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
582 self.network
583 );
584
585 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
586 info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
587 } else {
588 self.set_gateway_state(GatewayState::Syncing).await;
589 info!(target: LOG_GATEWAY, "Waiting for chain sync");
590 if let Err(err) = ln_client.wait_for_chain_sync().await {
591 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
592 return ReceivePaymentStreamAction::RetryAfterDelay;
593 }
594 }
595
596 let lightning_context = LightningContext {
597 lnrpc: ln_client,
598 lightning_public_key,
599 lightning_alias,
600 lightning_network,
601 };
602 self.set_gateway_state(GatewayState::Running { lightning_context })
603 .await;
604 info!(target: LOG_GATEWAY, "Gateway is running");
605
606 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
607 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
610 let all_federations_configs =
611 dbtx.load_federation_configs().await.into_iter().collect();
612 self.register_federations(&all_federations_configs, &self.task_group)
613 .await;
614 }
615
616 if handle
619 .cancel_on_shutdown(async move {
620 loop {
621 let payment_request_or = tokio::select! {
622 payment_request_or = stream.next() => {
623 payment_request_or
624 }
625 () = self.is_shutting_down_safely() => {
626 break;
627 }
628 };
629
630 let Some(payment_request) = payment_request_or else {
631 warn!(
632 target: LOG_GATEWAY,
633 "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
634 );
635 break;
636 };
637
638 let state_guard = self.state.read().await;
639 if let GatewayState::Running { ref lightning_context } = *state_guard {
640 self.handle_lightning_payment(payment_request, lightning_context).await;
641 } else {
642 warn!(
643 target: LOG_GATEWAY,
644 state = %state_guard,
645 "Gateway isn't in a running state, cannot handle incoming payments."
646 );
647 break;
648 }
649 }
650 })
651 .await
652 .is_ok()
653 {
654 warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
655 ReceivePaymentStreamAction::RetryAfterDelay
656 } else {
657 info!(target: LOG_GATEWAY, "Received shutdown signal");
658 ReceivePaymentStreamAction::NoRetry
659 }
660 }
661
662 async fn is_shutting_down_safely(&self) {
665 loop {
666 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
667 return;
668 }
669
670 fedimint_core::task::sleep(Duration::from_secs(1)).await;
671 }
672 }
673
674 async fn handle_lightning_payment(
679 &self,
680 payment_request: InterceptPaymentRequest,
681 lightning_context: &LightningContext,
682 ) {
683 info!(
684 target: LOG_GATEWAY,
685 lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
686 "Intercepting lightning payment",
687 );
688
689 if self
690 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
691 .await
692 .is_ok()
693 {
694 return;
695 }
696
697 if self
698 .try_handle_lightning_payment_ln_legacy(&payment_request)
699 .await
700 .is_ok()
701 {
702 return;
703 }
704
705 Self::forward_lightning_payment(payment_request, lightning_context).await;
706 }
707
708 async fn try_handle_lightning_payment_lnv2(
711 &self,
712 htlc_request: &InterceptPaymentRequest,
713 lightning_context: &LightningContext,
714 ) -> Result<()> {
715 let (contract, client) = self
721 .get_registered_incoming_contract_and_client_v2(
722 PaymentImage::Hash(htlc_request.payment_hash),
723 htlc_request.amount_msat,
724 )
725 .await?;
726
727 if let Err(err) = client
728 .get_first_module::<GatewayClientModuleV2>()
729 .expect("Must have client module")
730 .relay_incoming_htlc(
731 htlc_request.payment_hash,
732 htlc_request.incoming_chan_id,
733 htlc_request.htlc_id,
734 contract,
735 htlc_request.amount_msat,
736 )
737 .await
738 {
739 warn!(target: LOG_GATEWAY, err = %err.fmt_compact_anyhow(), "Error relaying incoming lightning payment");
740
741 let outcome = InterceptPaymentResponse {
742 action: PaymentAction::Cancel,
743 payment_hash: htlc_request.payment_hash,
744 incoming_chan_id: htlc_request.incoming_chan_id,
745 htlc_id: htlc_request.htlc_id,
746 };
747
748 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
749 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
750 }
751 }
752
753 Ok(())
754 }
755
756 async fn try_handle_lightning_payment_ln_legacy(
759 &self,
760 htlc_request: &InterceptPaymentRequest,
761 ) -> Result<()> {
762 let Some(federation_index) = htlc_request.short_channel_id else {
764 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
765 "Incoming payment has not last hop short channel id".to_string(),
766 )));
767 };
768
769 let Some(client) = self
770 .federation_manager
771 .read()
772 .await
773 .get_client_for_index(federation_index)
774 else {
775 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())));
776 };
777
778 client
779 .borrow()
780 .with(|client| async {
781 let htlc = htlc_request.clone().try_into();
782 match htlc {
783 Ok(htlc) => {
784 match client
785 .get_first_module::<GatewayClientModule>()
786 .expect("Must have client module")
787 .gateway_handle_intercepted_htlc(htlc)
788 .await
789 {
790 Ok(_) => Ok(()),
791 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
792 format!("Error intercepting lightning payment {e:?}"),
793 ))),
794 }
795 }
796 _ => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
797 "Could not convert InterceptHtlcResult into an HTLC".to_string(),
798 ))),
799 }
800 })
801 .await
802 }
803
804 async fn forward_lightning_payment(
808 htlc_request: InterceptPaymentRequest,
809 lightning_context: &LightningContext,
810 ) {
811 let outcome = InterceptPaymentResponse {
812 action: PaymentAction::Forward,
813 payment_hash: htlc_request.payment_hash,
814 incoming_chan_id: htlc_request.incoming_chan_id,
815 htlc_id: htlc_request.htlc_id,
816 };
817
818 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
819 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
820 }
821 }
822
823 async fn set_gateway_state(&self, state: GatewayState) {
825 let mut lock = self.state.write().await;
826 *lock = state;
827 }
828
829 pub async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
832 let GatewayState::Running { lightning_context } = self.get_state().await else {
833 return Ok(GatewayInfo {
834 federations: vec![],
835 federation_fake_scids: None,
836 version_hash: fedimint_build_code_version_env!().to_string(),
837 lightning_pub_key: None,
838 lightning_alias: None,
839 gateway_id: self.gateway_id,
840 gateway_state: self.state.read().await.to_string(),
841 network: self.network,
842 block_height: None,
843 synced_to_chain: false,
844 api: self.versioned_api.clone(),
845 lightning_mode: self.lightning_mode.clone(),
846 });
847 };
848
849 let dbtx = self.gateway_db.begin_transaction_nc().await;
850 let federations = self
851 .federation_manager
852 .read()
853 .await
854 .federation_info_all_federations(dbtx)
855 .await;
856
857 let channels: BTreeMap<u64, FederationId> = federations
858 .iter()
859 .map(|federation_info| {
860 (
861 federation_info.config.federation_index,
862 federation_info.federation_id,
863 )
864 })
865 .collect();
866
867 let node_info = lightning_context.lnrpc.parsed_node_info().await?;
868
869 Ok(GatewayInfo {
870 federations,
871 federation_fake_scids: Some(channels),
872 version_hash: fedimint_build_code_version_env!().to_string(),
873 lightning_pub_key: Some(lightning_context.lightning_public_key.to_string()),
874 lightning_alias: Some(lightning_context.lightning_alias.clone()),
875 gateway_id: self.gateway_id,
876 gateway_state: self.state.read().await.to_string(),
877 network: self.network,
878 block_height: Some(node_info.3),
879 synced_to_chain: node_info.4,
880 api: self.versioned_api.clone(),
881 lightning_mode: self.lightning_mode.clone(),
882 })
883 }
884
885 pub async fn handle_get_federation_config(
888 &self,
889 federation_id_or: Option<FederationId>,
890 ) -> AdminResult<GatewayFedConfig> {
891 if !matches!(self.get_state().await, GatewayState::Running { .. }) {
892 return Ok(GatewayFedConfig {
893 federations: BTreeMap::new(),
894 });
895 }
896
897 let federations = if let Some(federation_id) = federation_id_or {
898 let mut federations = BTreeMap::new();
899 federations.insert(
900 federation_id,
901 self.federation_manager
902 .read()
903 .await
904 .get_federation_config(federation_id)
905 .await?,
906 );
907 federations
908 } else {
909 self.federation_manager
910 .read()
911 .await
912 .get_all_federation_configs()
913 .await
914 };
915
916 Ok(GatewayFedConfig { federations })
917 }
918
919 pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
922 let (_, address, _) = self
923 .select_client(payload.federation_id)
924 .await?
925 .value()
926 .get_first_module::<WalletClientModule>()
927 .expect("Must have client module")
928 .allocate_deposit_address_expert_only(())
929 .await?;
930 Ok(address)
931 }
932
933 pub async fn handle_withdraw_msg(
936 &self,
937 payload: WithdrawPayload,
938 ) -> AdminResult<WithdrawResponse> {
939 let WithdrawPayload {
940 amount,
941 address,
942 federation_id,
943 } = payload;
944
945 let address_network = get_network_for_address(&address);
946 let gateway_network = self.network;
947 let Ok(address) = address.require_network(gateway_network) else {
948 return Err(AdminGatewayError::WithdrawError {
949 failure_reason: format!(
950 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
951 ),
952 });
953 };
954
955 let client = self.select_client(federation_id).await?;
956 let wallet_module = client.value().get_first_module::<WalletClientModule>()?;
957
958 let (amount, fees) = match amount {
960 BitcoinAmountOrAll::All => {
963 let balance = bitcoin::Amount::from_sat(
964 client
965 .value()
966 .get_balance()
967 .await
968 .ok_or_else(|| {
969 AdminGatewayError::Unexpected(anyhow!("Primary module not available"))
970 })?
971 .msats
972 / 1000,
973 );
974 let fees = wallet_module.get_withdraw_fees(&address, balance).await?;
975 let withdraw_amount = balance.checked_sub(fees.amount());
976 if withdraw_amount.is_none() {
977 return Err(AdminGatewayError::WithdrawError {
978 failure_reason: format!(
979 "Insufficient funds. Balance: {balance} Fees: {fees:?}"
980 ),
981 });
982 }
983 (withdraw_amount.unwrap(), fees)
984 }
985 BitcoinAmountOrAll::Amount(amount) => (
986 amount,
987 wallet_module.get_withdraw_fees(&address, amount).await?,
988 ),
989 };
990
991 let operation_id = wallet_module.withdraw(&address, amount, fees, ()).await?;
992 let mut updates = wallet_module
993 .subscribe_withdraw_updates(operation_id)
994 .await?
995 .into_stream();
996
997 while let Some(update) = updates.next().await {
998 match update {
999 WithdrawState::Succeeded(txid) => {
1000 info!(target: LOG_GATEWAY, amount = %amount, address = %address, "Sent funds");
1001 return Ok(WithdrawResponse { txid, fees });
1002 }
1003 WithdrawState::Failed(e) => {
1004 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
1005 }
1006 WithdrawState::Created => {}
1007 }
1008 }
1009
1010 Err(AdminGatewayError::WithdrawError {
1011 failure_reason: "Ran out of state updates while withdrawing".to_string(),
1012 })
1013 }
1014
1015 async fn handle_create_invoice_for_operator_msg(
1018 &self,
1019 payload: CreateInvoiceForOperatorPayload,
1020 ) -> AdminResult<Bolt11Invoice> {
1021 let GatewayState::Running { lightning_context } = self.get_state().await else {
1022 return Err(AdminGatewayError::Lightning(
1023 LightningRpcError::FailedToConnect,
1024 ));
1025 };
1026
1027 Bolt11Invoice::from_str(
1028 &lightning_context
1029 .lnrpc
1030 .create_invoice(CreateInvoiceRequest {
1031 payment_hash: None, amount_msat: payload.amount_msats,
1034 expiry_secs: payload.expiry_secs.unwrap_or(3600),
1035 description: payload.description.map(InvoiceDescription::Direct),
1036 })
1037 .await?
1038 .invoice,
1039 )
1040 .map_err(|e| {
1041 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
1042 failure_reason: e.to_string(),
1043 })
1044 })
1045 }
1046
1047 async fn handle_pay_invoice_for_operator_msg(
1050 &self,
1051 payload: PayInvoiceForOperatorPayload,
1052 ) -> AdminResult<Preimage> {
1053 const BASE_FEE: u64 = 50;
1055 const FEE_DENOMINATOR: u64 = 100;
1056 const MAX_DELAY: u64 = 1008;
1057
1058 let GatewayState::Running { lightning_context } = self.get_state().await else {
1059 return Err(AdminGatewayError::Lightning(
1060 LightningRpcError::FailedToConnect,
1061 ));
1062 };
1063
1064 let max_fee = BASE_FEE
1065 + payload
1066 .invoice
1067 .amount_milli_satoshis()
1068 .context("Invoice is missing amount")?
1069 .saturating_div(FEE_DENOMINATOR);
1070
1071 let res = lightning_context
1072 .lnrpc
1073 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
1074 .await?;
1075 Ok(res.preimage)
1076 }
1077
1078 async fn handle_pay_invoice_msg(
1081 &self,
1082 payload: fedimint_ln_client::pay::PayInvoicePayload,
1083 ) -> Result<Preimage> {
1084 let GatewayState::Running { .. } = self.get_state().await else {
1085 return Err(PublicGatewayError::Lightning(
1086 LightningRpcError::FailedToConnect,
1087 ));
1088 };
1089
1090 debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1091 let client = self.select_client(payload.federation_id).await?;
1092 let contract_id = payload.contract_id;
1093 let gateway_module = &client
1094 .value()
1095 .get_first_module::<GatewayClientModule>()
1096 .map_err(LNv1Error::OutgoingPayment)
1097 .map_err(PublicGatewayError::LNv1)?;
1098 let operation_id = gateway_module
1099 .gateway_pay_bolt11_invoice(payload)
1100 .await
1101 .map_err(LNv1Error::OutgoingPayment)
1102 .map_err(PublicGatewayError::LNv1)?;
1103 let mut updates = gateway_module
1104 .gateway_subscribe_ln_pay(operation_id)
1105 .await
1106 .map_err(LNv1Error::OutgoingPayment)
1107 .map_err(PublicGatewayError::LNv1)?
1108 .into_stream();
1109 while let Some(update) = updates.next().await {
1110 match update {
1111 GatewayExtPayStates::Success { preimage, .. } => {
1112 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1113 return Ok(preimage);
1114 }
1115 GatewayExtPayStates::Fail {
1116 error,
1117 error_message,
1118 } => {
1119 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1120 error: Box::new(error),
1121 message: format!(
1122 "{error_message} while paying invoice with contract id {contract_id}"
1123 ),
1124 }));
1125 }
1126 GatewayExtPayStates::Canceled { error } => {
1127 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1128 error: Box::new(error.clone()),
1129 message: format!(
1130 "Cancelled with {error} while paying invoice with contract id {contract_id}"
1131 ),
1132 }));
1133 }
1134 GatewayExtPayStates::Created => {
1135 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1136 }
1137 other => {
1138 debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1139 }
1140 }
1141 }
1142
1143 Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1144 anyhow!("Ran out of state updates while paying invoice"),
1145 )))
1146 }
1147
1148 pub async fn handle_connect_federation(
1153 &self,
1154 payload: ConnectFedPayload,
1155 ) -> AdminResult<FederationInfo> {
1156 let GatewayState::Running { lightning_context } = self.get_state().await else {
1157 return Err(AdminGatewayError::Lightning(
1158 LightningRpcError::FailedToConnect,
1159 ));
1160 };
1161
1162 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
1163 AdminGatewayError::ClientCreationError(anyhow!(format!(
1164 "Invalid federation member string {e:?}"
1165 )))
1166 })?;
1167
1168 #[cfg(feature = "tor")]
1169 let connector = match &payload.use_tor {
1170 Some(true) => Connector::tor(),
1171 Some(false) => Connector::default(),
1172 None => {
1173 debug!(target: LOG_GATEWAY, "Missing `use_tor` payload field, defaulting to `Connector::Tcp` variant!");
1174 Connector::default()
1175 }
1176 };
1177
1178 #[cfg(not(feature = "tor"))]
1179 let connector = Connector::default();
1180
1181 let federation_id = invite_code.federation_id();
1182
1183 let mut federation_manager = self.federation_manager.write().await;
1184
1185 if federation_manager.has_federation(federation_id) {
1187 return Err(AdminGatewayError::ClientCreationError(anyhow!(
1188 "Federation has already been registered"
1189 )));
1190 }
1191
1192 let federation_index = federation_manager.pop_next_index()?;
1195
1196 let federation_config = FederationConfig {
1197 invite_code,
1198 federation_index,
1199 lightning_fee: self.default_routing_fees,
1200 transaction_fee: self.default_transaction_fees,
1201 connector,
1202 };
1203
1204 let recover = payload.recover.unwrap_or(false);
1205 if recover {
1206 self.client_builder
1207 .recover(
1208 federation_config.clone(),
1209 Arc::new(self.clone()),
1210 &self.mnemonic,
1211 )
1212 .await?;
1213 }
1214
1215 let client = self
1216 .client_builder
1217 .build(
1218 federation_config.clone(),
1219 Arc::new(self.clone()),
1220 &self.mnemonic,
1221 )
1222 .await?;
1223
1224 if recover {
1225 client.wait_for_all_active_state_machines().await?;
1226 }
1227
1228 let federation_info = FederationInfo {
1231 federation_id,
1232 federation_name: federation_manager.federation_name(&client).await,
1233 balance_msat: client.get_balance().await.unwrap_or_else(|| {
1234 warn!(
1235 target: LOG_GATEWAY,
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}