1#![deny(clippy::pedantic)]
2#![allow(clippy::cast_possible_truncation)]
3#![allow(clippy::cast_possible_wrap)]
4#![allow(clippy::cast_sign_loss)]
5#![allow(clippy::default_trait_access)]
6#![allow(clippy::doc_markdown)]
7#![allow(clippy::missing_errors_doc)]
8#![allow(clippy::missing_panics_doc)]
9#![allow(clippy::module_name_repetitions)]
10#![allow(clippy::must_use_candidate)]
11#![allow(clippy::return_self_not_must_use)]
12#![allow(clippy::similar_names)]
13#![allow(clippy::too_many_lines)]
14#![allow(clippy::large_futures)]
15#![allow(clippy::struct_field_names)]
16
17pub mod client;
18pub mod config;
19pub mod envs;
20mod error;
21mod events;
22mod federation_manager;
23mod federation_status;
24mod iroh_server;
25mod metrics;
26mod rate_limit;
27mod registration_health;
28pub mod rpc_server;
29mod types;
30
31use std::collections::{BTreeMap, BTreeSet};
32use std::env;
33use std::fmt::Display;
34use std::net::SocketAddr;
35use std::str::FromStr;
36use std::sync::Arc;
37use std::time::{Duration, UNIX_EPOCH};
38
39use anyhow::{Context, anyhow};
40use async_trait::async_trait;
41use bitcoin::hashes::sha256;
42use bitcoin::{Address, Network, Txid, secp256k1};
43use clap::Parser;
44use client::GatewayClientBuilder;
45pub use config::GatewayParameters;
46use config::{DatabaseBackend, GatewayOpts};
47use envs::FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV;
48use error::FederationNotConnected;
49use events::ALL_GATEWAY_EVENTS;
50use federation_manager::FederationManager;
51use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic};
52use fedimint_bitcoind::bitcoincore::BitcoindClient;
53use fedimint_bitcoind::{EsploraClient, IBitcoindRpc};
54use fedimint_client::module_init::ClientModuleInitRegistry;
55use fedimint_client::secret::RootSecretStrategy;
56use fedimint_client::{Client, ClientHandleArc};
57use fedimint_core::base32::{self, FEDIMINT_PREFIX};
58use fedimint_core::config::FederationId;
59use fedimint_core::core::OperationId;
60use fedimint_core::db::{Committable, Database, DatabaseTransaction, apply_migrations};
61use fedimint_core::envs::is_env_var_set;
62use fedimint_core::invite_code::InviteCode;
63use fedimint_core::module::CommonModuleInit;
64use fedimint_core::module::registry::ModuleDecoderRegistry;
65use fedimint_core::rustls::install_crypto_provider;
66use fedimint_core::secp256k1::PublicKey;
67use fedimint_core::secp256k1::schnorr::Signature;
68use fedimint_core::task::{TaskGroup, TaskHandle, TaskShutdownToken, sleep, timeout};
69use fedimint_core::time::duration_since_epoch;
70use fedimint_core::util::backoff_util::fibonacci_max_one_hour;
71use fedimint_core::util::{FmtCompact, SafeUrl, Spanned, retry};
72use fedimint_core::{
73 Amount, BitcoinAmountOrAll, PeerId, TieredCounts, crit, fedimint_build_code_version_env,
74 get_network_for_address,
75};
76use fedimint_eventlog::{DBTransactionEventLogExt, EventLogId, StructuredPaymentEvents};
77use fedimint_gateway_common::{
78 BackupPayload, ChainSource, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse,
79 ConnectFedPayload, ConnectPeerRequest, ConnectorType, CreateInvoiceForOperatorPayload,
80 CreateOfferPayload, CreateOfferResponse, DepositAddressPayload, DepositAddressRecheckPayload,
81 FederationBalanceInfo, FederationConfig, FederationInfo, GatewayBalances, GatewayFedConfig,
82 GatewayInfo, GetInvoiceRequest, GetInvoiceResponse, LeaveFedPayload, LightningInfo,
83 LightningMode, ListTransactionsPayload, ListTransactionsResponse, MnemonicResponse,
84 OpenChannelRequest, PayInvoiceForOperatorPayload, PayOfferPayload, PayOfferResponse,
85 PaymentLogPayload, PaymentLogResponse, PaymentPolicy, PaymentPolicyGuard, PaymentStats,
86 PaymentSummaryPayload, PaymentSummaryResponse, PeginFromOnchainPayload, ReceiveEcashPayload,
87 ReceiveEcashResponse, RegisteredProtocol, SendOnchainRequest, SetChannelFeesRequest,
88 SetFeesPayload, SetMnemonicPayload, SetPaymentPolicyPayload, SpendEcashPayload,
89 SpendEcashResponse, V1_API_ENDPOINT, WithdrawPayload, WithdrawPreviewPayload,
90 WithdrawPreviewResponse, WithdrawResponse, WithdrawToOnchainPayload,
91};
92use fedimint_gateway_server_db::{GatewayDbtxNcExt as _, get_gatewayd_database_migrations};
93pub use fedimint_gateway_ui::IAdminGateway;
94use fedimint_gw_client::events::compute_lnv1_stats;
95use fedimint_gw_client::pay::{OutgoingPaymentError, OutgoingPaymentErrorType};
96use fedimint_gw_client::{
97 GatewayClientModule, GatewayClientV1Error, GatewayExtPayStates, GatewayExtReceiveStates, Htlc,
98 IGatewayClientV1, SwapParameters,
99};
100use fedimint_gwv2_client::events::compute_lnv2_stats;
101use fedimint_gwv2_client::{
102 EXPIRATION_DELTA_MINIMUM_V2, FinalReceiveState, GatewayClientModuleV2, GatewayClientV2Error,
103 IGatewayClientV2,
104};
105use fedimint_lightning::ldk::MnemonicSource;
106use fedimint_lightning::lnd::GatewayLndClient;
107use fedimint_lightning::{
108 CreateInvoiceRequest, ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse,
109 InvoiceDescription, LightningContext, LightningRpcError, LnRpcTracked, Lnv2HoldInvoiceFilter,
110 PayInvoiceResponse, PaymentAction, RouteHtlcStream, ldk,
111};
112use fedimint_ln_client::pay::PaymentData;
113use fedimint_ln_common::config::LightningClientConfig;
114use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
115use fedimint_ln_common::contracts::{IdentifiableContract, Preimage};
116use fedimint_ln_common::{LightningCommonInit, PreimageAuth};
117use fedimint_lnurl::VerifyResponse;
118use fedimint_lnv2_common::Bolt11InvoiceDescription;
119use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
120use fedimint_lnv2_common::gateway_api::{
121 CreateBolt11InvoicePayload, MAX_INVOICE_EXPIRY_SECS, PaymentFee, RoutingInfo,
122 SendPaymentPayload,
123};
124use fedimint_logging::LOG_GATEWAY;
125use fedimint_mint_client::{MintClientInit, MintClientModule, OOBNotes, ReissueExternalNotesState};
126use fedimint_mintv2_client::{
127 MintClientInit as MintV2ClientInit, MintClientModule as MintV2ClientModule,
128};
129use fedimint_wallet_client::{PegOutFees, WalletClientInit, WalletClientModule, WithdrawState};
130use futures::stream::StreamExt;
131use lightning_invoice::{Bolt11Invoice, RoutingFees};
132use rand::rngs::OsRng;
133use tokio::sync::RwLock;
134use tracing::{debug, info, info_span, warn};
135
136use crate::envs::FM_GATEWAY_MNEMONIC_ENV;
137use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError};
138use crate::events::get_events_for_duration;
139use crate::rate_limit::TokenBucketRateLimiter;
140use crate::registration_health::RegistrationHealthTracker;
141use crate::rpc_server::run_webserver;
142use crate::types::PrettyInterceptPaymentRequest;
143
144const GW_ANNOUNCEMENT_TTL: Duration = Duration::from_mins(10);
146
147const DEFAULT_NUM_ROUTE_HINTS: u32 = 1;
150
151const DEFAULT_INVOICE_RATE_LIMIT_BURST: u32 = 50;
153
154const DEFAULT_INVOICE_RATE_LIMIT_PER_SECOND: u32 = 5;
157
158pub const DEFAULT_NETWORK: Network = Network::Regtest;
160
161const LIGHTNING_CONTEXT_RETRY_INTERVAL: Duration = Duration::from_secs(5);
164
165const VERIFY_WAIT_TIMEOUT: Duration = Duration::from_secs(30);
173
174pub type Result<T> = std::result::Result<T, PublicGatewayError>;
175pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
176
177const DB_FILE: &str = "gatewayd.db";
180
181const LDK_NODE_DB_FOLDER: &str = "ldk_node";
184
185#[derive(Clone, Debug)]
198pub enum GatewayState {
199 NotConfigured {
200 mnemonic_sender: tokio::sync::broadcast::Sender<MnemonicSource>,
203 },
204 Disconnected,
205 Syncing,
206 Connected,
207 Running {
208 lightning_context: LightningContext,
209 },
210 ShuttingDown {
211 lightning_context: LightningContext,
212 },
213}
214
215impl Display for GatewayState {
216 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
217 match self {
218 GatewayState::NotConfigured { .. } => write!(f, "NotConfigured"),
219 GatewayState::Disconnected => write!(f, "Disconnected"),
220 GatewayState::Syncing => write!(f, "Syncing"),
221 GatewayState::Connected => write!(f, "Connected"),
222 GatewayState::Running { .. } => write!(f, "Running"),
223 GatewayState::ShuttingDown { .. } => write!(f, "ShuttingDown"),
224 }
225 }
226}
227
228#[derive(Debug, Clone)]
231struct Registration {
232 endpoint_url: SafeUrl,
234
235 keypair: secp256k1::Keypair,
237}
238
239impl Registration {
240 pub async fn new(db: &Database, endpoint_url: SafeUrl, protocol: RegisteredProtocol) -> Self {
241 let keypair = Gateway::load_or_create_gateway_keypair(db, protocol).await;
242 Self {
243 endpoint_url,
244 keypair,
245 }
246 }
247}
248
249#[bon::bon]
250impl Gateway {
251 #[builder(start_fn = builder, finish_fn = build)]
266 pub async fn new_with_builder(
267 #[builder(start_fn)] lightning_mode: LightningMode,
268 #[builder(start_fn)] client_builder: GatewayClientBuilder,
269 #[builder(start_fn)] gateway_db: Database,
270 bcrypt_password_hash: bcrypt::HashParts,
271 bcrypt_liquidity_manager_password_hash: Option<bcrypt::HashParts>,
272 gateway_state: GatewayState,
273 chain_source: ChainSource,
274 #[builder(default = ([127, 0, 0, 1], 80).into())] listen: SocketAddr,
275 api_addr: Option<SafeUrl>,
276 #[builder(default = DEFAULT_NETWORK)] network: Network,
277 #[builder(default = DEFAULT_NUM_ROUTE_HINTS)] num_route_hints: u32,
278 #[builder(default = PaymentFee::TRANSACTION_FEE_DEFAULT)] default_routing_fees: PaymentFee,
279 #[builder(default = PaymentFee::TRANSACTION_FEE_DEFAULT)]
280 default_transaction_fees: PaymentFee,
281 iroh_listen: Option<SocketAddr>,
282 iroh_dns: Option<SafeUrl>,
283 #[builder(default)] iroh_relays: Vec<SafeUrl>,
284 metrics_listen: Option<SocketAddr>,
285 ) -> anyhow::Result<Gateway> {
286 let versioned_api = api_addr.map(|addr| {
287 addr.join(V1_API_ENDPOINT)
288 .expect("Failed to version gateway API address")
289 });
290
291 let metrics_listen = metrics_listen.unwrap_or_else(|| {
292 SocketAddr::new(
293 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
294 listen.port() + 1,
295 )
296 });
297
298 Gateway::new(
299 lightning_mode,
300 GatewayParameters {
301 listen,
302 versioned_api,
303 bcrypt_password_hash,
304 bcrypt_liquidity_manager_password_hash,
305 network,
306 num_route_hints,
307 default_routing_fees,
308 default_transaction_fees,
309 iroh_listen,
310 iroh_dns,
311 iroh_relays,
312 skip_setup: true,
313 metrics_listen,
314 invoice_rate_limit_burst: DEFAULT_INVOICE_RATE_LIMIT_BURST,
315 invoice_rate_limit_per_second: DEFAULT_INVOICE_RATE_LIMIT_PER_SECOND,
316 },
317 gateway_db,
318 client_builder,
319 gateway_state,
320 chain_source,
321 MnemonicSource::Existing,
322 )
323 .await
324 }
325}
326
327enum ReceivePaymentStreamAction {
329 RetryAfterDelay,
330 NoRetry,
331}
332
333#[derive(Clone)]
334pub struct Gateway {
335 federation_manager: Arc<RwLock<FederationManager>>,
337
338 lightning_mode: LightningMode,
340
341 state: Arc<RwLock<GatewayState>>,
343
344 client_builder: GatewayClientBuilder,
347
348 gateway_db: Database,
350
351 listen: SocketAddr,
353
354 metrics_listen: SocketAddr,
356
357 task_group: TaskGroup,
359
360 bcrypt_password_hash: String,
362
363 bcrypt_liquidity_manager_password_hash: Option<String>,
366
367 num_route_hints: u32,
369
370 network: Network,
372
373 chain_source: ChainSource,
375
376 default_routing_fees: PaymentFee,
378
379 default_transaction_fees: PaymentFee,
381
382 iroh_sk: iroh::SecretKey,
384
385 iroh_listen: Option<SocketAddr>,
387
388 iroh_dns: Option<SafeUrl>,
390
391 iroh_relays: Vec<SafeUrl>,
394
395 registrations: BTreeMap<RegisteredProtocol, Registration>,
398
399 registration_health: RegistrationHealthTracker,
401
402 invoice_rate_limiter: Arc<TokenBucketRateLimiter>,
404
405 lnv2_hold_invoice_lock_pool: Arc<lockable::LockPool<sha256::Hash>>,
409
410 payment_policy_lock: Arc<RwLock<()>>,
413
414 mnemonic_source: MnemonicSource,
418}
419
420impl std::fmt::Debug for Gateway {
421 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422 f.debug_struct("Gateway")
423 .field("federation_manager", &self.federation_manager)
424 .field("state", &self.state)
425 .field("client_builder", &self.client_builder)
426 .field("gateway_db", &self.gateway_db)
427 .field("listen", &self.listen)
428 .field("registrations", &self.registrations)
429 .finish_non_exhaustive()
430 }
431}
432
433struct WithdrawDetails {
435 amount: Amount,
436 mint_fees: Option<Amount>,
437 peg_out_fees: PegOutFees,
438 flat_peg_out_fee: bool,
439}
440
441async fn await_withdraw_change(
450 client: &ClientHandleArc,
451 operation_id: OperationId,
452 change: Vec<fedimint_core::OutPoint>,
453) {
454 if let Err(err) = client
455 .await_primary_bitcoin_module_outputs(operation_id, change)
456 .await
457 {
458 warn!(
459 target: LOG_GATEWAY,
460 err = %err.fmt_compact(),
461 ?operation_id,
462 "Failed to await withdrawal change outputs"
463 );
464 }
465}
466
467async fn withdraw_v2(
469 client: &ClientHandleArc,
470 wallet_module: &fedimint_walletv2_client::WalletClientModule,
471 address: &Address,
472 amount: BitcoinAmountOrAll,
473 quoted_fee: Option<bitcoin::Amount>,
474) -> AdminResult<WithdrawResponse> {
475 let current_fee =
476 wallet_module
477 .send_fee()
478 .await
479 .map_err(|e| AdminGatewayError::WithdrawError {
480 failure_reason: e.fmt_compact().to_string(),
481 })?;
482
483 let fee = match quoted_fee {
488 Some(quoted_fee) if quoted_fee < current_fee => {
489 return Err(AdminGatewayError::WithdrawError {
490 failure_reason: format!(
491 "The on-chain fee increased from {} to {} sats since the preview, please preview the withdrawal again",
492 quoted_fee.to_sat(),
493 current_fee.to_sat()
494 ),
495 });
496 }
497 Some(quoted_fee) => quoted_fee,
498 None => current_fee,
499 };
500
501 let withdraw_amount = match amount {
502 BitcoinAmountOrAll::All => {
503 let balance = client.get_balance_for_btc().await.map_err(|err| {
504 AdminGatewayError::Unexpected(anyhow!(
505 "Balance not available: {}",
506 err.fmt_compact()
507 ))
508 })?;
509
510 wallet_module
515 .max_sendable_amount(balance, fee)
516 .await
517 .map_err(|err| AdminGatewayError::WithdrawError {
518 failure_reason: format!(
519 "Insufficient funds. Balance: {balance} Fee: {fee}: {}",
520 err.fmt_compact()
521 ),
522 })?
523 }
524 BitcoinAmountOrAll::Amount(a) => a,
525 };
526
527 let operation_id = wallet_module
528 .send(
529 address.as_unchecked().clone(),
530 withdraw_amount,
531 Some(fee),
532 serde_json::Value::Null,
533 )
534 .await
535 .map_err(|e| AdminGatewayError::WithdrawError {
536 failure_reason: e.fmt_compact().to_string(),
537 })?;
538
539 let result = wallet_module
540 .await_final_send_operation_state(operation_id)
541 .await
542 .map_err(|e| AdminGatewayError::WithdrawError {
543 failure_reason: e.fmt_compact().to_string(),
544 })?;
545
546 let fees = PegOutFees::from_amount(fee);
547
548 match result {
549 fedimint_walletv2_client::FinalSendOperationState::Success(txid) => {
550 if let Some(Ok(fedimint_walletv2_client::WalletOperationMeta::Send(meta))) = client
551 .operation_log()
552 .get_operation(operation_id)
553 .await
554 .map(|operation| operation.try_meta())
555 {
556 await_withdraw_change(
557 client,
558 operation_id,
559 meta.change_outpoint_range.into_iter().collect(),
560 )
561 .await;
562 }
563 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds via walletv2");
564 Ok(WithdrawResponse { txid, fees })
565 }
566 fedimint_walletv2_client::FinalSendOperationState::Aborted => {
567 Err(AdminGatewayError::WithdrawError {
568 failure_reason: "Withdrawal transaction was aborted".to_string(),
569 })
570 }
571 fedimint_walletv2_client::FinalSendOperationState::Failure => {
572 Err(AdminGatewayError::WithdrawError {
573 failure_reason: "Withdrawal failed".to_string(),
574 })
575 }
576 }
577}
578
579async fn calculate_max_withdrawable(
581 client: &ClientHandleArc,
582 address: &Address,
583) -> AdminResult<WithdrawDetails> {
584 let balance = client.get_balance_for_btc().await.map_err(|err| {
585 AdminGatewayError::Unexpected(anyhow!("Balance not available: {}", err.fmt_compact()))
586 })?;
587
588 if let Ok(wallet_module) =
589 client.get_first_module::<fedimint_walletv2_client::WalletClientModule>()
590 {
591 let fee = wallet_module
592 .send_fee()
593 .await
594 .map_err(|e| AdminGatewayError::WithdrawError {
595 failure_reason: e.fmt_compact().to_string(),
596 })?;
597
598 let max_withdrawable = wallet_module
599 .max_sendable_amount(balance, fee)
600 .await
601 .map_err(|err| AdminGatewayError::WithdrawError {
602 failure_reason: err.fmt_compact().to_string(),
603 })?;
604
605 let federation_fees = balance
610 .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
611 .saturating_sub(Amount::from_sats(fee.to_sat()));
612
613 return Ok(WithdrawDetails {
614 amount: Amount::from_sats(max_withdrawable.to_sat()),
615 mint_fees: Some(federation_fees),
616 peg_out_fees: PegOutFees::from_amount(fee),
617 flat_peg_out_fee: true,
618 });
619 }
620
621 let Ok(wallet_module) = client.get_first_module::<WalletClientModule>() else {
622 return Err(AdminGatewayError::Unexpected(anyhow!(
623 "No wallet module found"
624 )));
625 };
626
627 let (max_withdrawable, peg_out_fees) = wallet_module
628 .max_withdrawable_amount(address, balance)
629 .await
630 .map_err(|err| AdminGatewayError::WithdrawError {
631 failure_reason: err.fmt_compact().to_string(),
632 })?;
633
634 let federation_fees = balance
639 .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
640 .saturating_sub(Amount::from_sats(peg_out_fees.amount().to_sat()));
641
642 Ok(WithdrawDetails {
643 amount: Amount::from_sats(max_withdrawable.to_sat()),
644 mint_fees: Some(federation_fees),
645 peg_out_fees,
646 flat_peg_out_fee: false,
647 })
648}
649
650impl Gateway {
651 fn get_bitcoind_client(
654 opts: &GatewayOpts,
655 network: bitcoin::Network,
656 gateway_id: &PublicKey,
657 ) -> anyhow::Result<(BitcoindClient, ChainSource)> {
658 let bitcoind_username = opts
659 .bitcoind_username
660 .clone()
661 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
662 let url = opts.bitcoind_url.clone().expect("No bitcoind url set");
663 let password = opts
664 .bitcoind_password
665 .clone()
666 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
667
668 let chain_source = ChainSource::Bitcoind {
669 username: bitcoind_username.clone(),
670 password: password.clone(),
671 server_url: url.clone(),
672 };
673 let wallet_name = format!("gatewayd-{gateway_id}");
674 let client = BitcoindClient::new(&url, bitcoind_username, password, &wallet_name, network)?;
675 Ok((client, chain_source))
676 }
677
678 pub async fn new_with_default_modules(
681 mnemonic_sender: tokio::sync::broadcast::Sender<MnemonicSource>,
682 ) -> anyhow::Result<Gateway> {
683 let opts = GatewayOpts::parse();
684 let gateway_parameters = opts.to_gateway_parameters()?;
685 let decoders = ModuleDecoderRegistry::default();
686
687 let db_path = opts.data_dir.join(DB_FILE);
688 let gateway_db = match opts.db_backend {
689 DatabaseBackend::RocksDb => {
690 debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
691 Database::new(
692 fedimint_rocksdb::RocksDb::build(db_path).open().await?,
693 decoders,
694 )
695 }
696 DatabaseBackend::CursedRedb => {
697 debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
698 Database::new(
699 fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
700 decoders,
701 )
702 }
703 };
704
705 apply_migrations(
708 &gateway_db,
709 (),
710 "gatewayd".to_string(),
711 get_gatewayd_database_migrations(),
712 None,
713 None,
714 )
715 .await?;
716
717 let http_id = Self::load_or_create_gateway_keypair(&gateway_db, RegisteredProtocol::Http)
720 .await
721 .public_key();
722 let (dyn_bitcoin_rpc, chain_source) =
723 match (opts.bitcoind_url.as_ref(), opts.esplora_url.as_ref()) {
724 (Some(_), None) => {
725 let (client, chain_source) =
726 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
727 (client.into_dyn(), chain_source)
728 }
729 (None, Some(url)) => {
730 let client = EsploraClient::new(url)
731 .expect("Could not create EsploraClient")
732 .into_dyn();
733 let chain_source = ChainSource::Esplora {
734 server_url: url.clone(),
735 };
736 (client, chain_source)
737 }
738 (Some(_), Some(_)) => {
739 let (client, chain_source) =
741 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
742 (client.into_dyn(), chain_source)
743 }
744 _ => unreachable!("ArgGroup already enforced XOR relation"),
745 };
746
747 let mut registry = ClientModuleInitRegistry::new();
750 registry.attach(MintClientInit);
751 registry.attach(MintV2ClientInit);
752 registry.attach(WalletClientInit::new(dyn_bitcoin_rpc));
753 registry.attach(fedimint_walletv2_client::WalletClientInit);
754
755 let client_builder =
756 GatewayClientBuilder::new(opts.data_dir.clone(), registry, opts.db_backend).await?;
757
758 let (gateway_state, mnemonic_source) = if Self::load_mnemonic(&gateway_db).await.is_some() {
759 (GatewayState::Disconnected, MnemonicSource::Existing)
760 } else {
761 if gateway_parameters.skip_setup {
764 let (mnemonic, mnemonic_source) = if let Ok(words) =
765 std::env::var(FM_GATEWAY_MNEMONIC_ENV)
766 {
767 info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
768 let mnemonic = Mnemonic::parse_in_normalized(Language::English, words.as_str())
769 .map_err(|e| {
770 AdminGatewayError::MnemonicError(anyhow!(format!(
771 "Seed phrase provided in environment was invalid {e:?}"
772 )))
773 })?;
774 (mnemonic, MnemonicSource::Existing)
775 } else {
776 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
777 (
778 Bip39RootSecretStrategy::<12>::random(&mut OsRng),
779 MnemonicSource::Generated,
780 )
781 };
782
783 Client::store_encodable_client_secret(&gateway_db, mnemonic.to_entropy())
784 .await
785 .map_err(|err| AdminGatewayError::MnemonicError(err.into()))?;
786 (GatewayState::Disconnected, mnemonic_source)
787 } else {
788 (
791 GatewayState::NotConfigured { mnemonic_sender },
792 MnemonicSource::Existing,
793 )
794 }
795 };
796
797 info!(
798 target: LOG_GATEWAY,
799 version = %fedimint_build_code_version_env!(),
800 "Starting gatewayd",
801 );
802
803 Gateway::new(
804 opts.mode,
805 gateway_parameters,
806 gateway_db,
807 client_builder,
808 gateway_state,
809 chain_source,
810 mnemonic_source,
811 )
812 .await
813 }
814
815 async fn new(
818 lightning_mode: LightningMode,
819 gateway_parameters: GatewayParameters,
820 gateway_db: Database,
821 client_builder: GatewayClientBuilder,
822 gateway_state: GatewayState,
823 chain_source: ChainSource,
824 mnemonic_source: MnemonicSource,
825 ) -> anyhow::Result<Gateway> {
826 let num_route_hints = gateway_parameters.num_route_hints;
827 let network = gateway_parameters.network;
828
829 let task_group = TaskGroup::new();
830 task_group.install_kill_handler();
831
832 let mut registrations = BTreeMap::new();
833 if let Some(http_url) = gateway_parameters.versioned_api {
834 registrations.insert(
835 RegisteredProtocol::Http,
836 Registration::new(&gateway_db, http_url, RegisteredProtocol::Http).await,
837 );
838 }
839
840 let iroh_sk = Self::load_or_create_iroh_key(&gateway_db).await;
841 if gateway_parameters.iroh_listen.is_some() {
842 let endpoint_url = SafeUrl::parse(&format!("iroh://{}", iroh_sk.public()))?;
843 registrations.insert(
844 RegisteredProtocol::Iroh,
845 Registration::new(&gateway_db, endpoint_url, RegisteredProtocol::Iroh).await,
846 );
847 }
848
849 Ok(Self {
850 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
851 lightning_mode,
852 state: Arc::new(RwLock::new(gateway_state)),
853 client_builder,
854 gateway_db: gateway_db.clone(),
855 listen: gateway_parameters.listen,
856 metrics_listen: gateway_parameters.metrics_listen,
857 task_group,
858 bcrypt_password_hash: gateway_parameters.bcrypt_password_hash.to_string(),
859 bcrypt_liquidity_manager_password_hash: gateway_parameters
860 .bcrypt_liquidity_manager_password_hash
861 .map(|h| h.to_string()),
862 num_route_hints,
863 network,
864 chain_source,
865 default_routing_fees: gateway_parameters.default_routing_fees,
866 default_transaction_fees: gateway_parameters.default_transaction_fees,
867 iroh_sk,
868 iroh_dns: gateway_parameters.iroh_dns,
869 iroh_relays: gateway_parameters.iroh_relays,
870 iroh_listen: gateway_parameters.iroh_listen,
871 registrations,
872 registration_health: RegistrationHealthTracker::default(),
873 invoice_rate_limiter: Arc::new(TokenBucketRateLimiter::new(
874 gateway_parameters.invoice_rate_limit_burst,
875 gateway_parameters.invoice_rate_limit_per_second,
876 )),
877 lnv2_hold_invoice_lock_pool: Arc::new(lockable::LockPool::new()),
878 payment_policy_lock: Arc::new(RwLock::new(())),
879 mnemonic_source,
880 })
881 }
882
883 async fn load_or_create_gateway_keypair(
884 gateway_db: &Database,
885 protocol: RegisteredProtocol,
886 ) -> secp256k1::Keypair {
887 let mut dbtx = gateway_db.begin_transaction().await;
888 let keypair = dbtx.load_or_create_gateway_keypair(protocol).await;
889 dbtx.commit_tx().await;
890 keypair
891 }
892
893 async fn load_or_create_iroh_key(gateway_db: &Database) -> iroh::SecretKey {
896 let mut dbtx = gateway_db.begin_transaction().await;
897 let iroh_sk = dbtx.load_or_create_iroh_key().await;
898 dbtx.commit_tx().await;
899 iroh_sk
900 }
901
902 pub async fn http_gateway_id(&self) -> PublicKey {
903 Self::load_or_create_gateway_keypair(&self.gateway_db, RegisteredProtocol::Http)
904 .await
905 .public_key()
906 }
907
908 async fn get_state(&self) -> GatewayState {
909 self.state.read().await.clone()
910 }
911
912 pub async fn dump_database(
915 dbtx: &mut DatabaseTransaction<'_>,
916 prefix_names: Vec<String>,
917 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
918 dbtx.dump_database(prefix_names).await
919 }
920
921 pub async fn run(
926 self,
927 runtime: Arc<tokio::runtime::Runtime>,
928 mnemonic_receiver: tokio::sync::broadcast::Receiver<MnemonicSource>,
929 ) -> anyhow::Result<TaskShutdownToken> {
930 install_crypto_provider().await;
931 self.register_clients_timer();
932 self.load_clients().await?;
933 self.start_gateway(runtime, mnemonic_receiver.resubscribe());
934 self.spawn_backup_task();
935 self.spawn_prune_registered_contracts_task();
936 fedimint_metrics::spawn_api_server(self.metrics_listen, self.task_group.clone()).await?;
938 let handle = self.task_group.make_handle();
940 run_webserver(Arc::new(self), mnemonic_receiver.resubscribe()).await?;
941 let shutdown_receiver = handle.make_shutdown_rx();
942 Ok(shutdown_receiver)
943 }
944
945 fn spawn_backup_task(&self) {
948 let self_copy = self.clone();
949 self.task_group
950 .spawn_cancellable_silent("backup ecash", async move {
951 const BACKUP_UPDATE_INTERVAL: Duration = Duration::from_hours(1);
952 let mut interval = tokio::time::interval(BACKUP_UPDATE_INTERVAL);
953 interval.tick().await;
954 loop {
955 {
956 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
957 self_copy.backup_all_federations(&mut dbtx).await;
958 dbtx.commit_tx().await;
959 interval.tick().await;
960 }
961 }
962 });
963 }
964
965 fn spawn_prune_registered_contracts_task(&self) {
969 let self_copy = self.clone();
970 self.task_group.spawn_cancellable_silent(
971 "prune registered incoming contracts",
972 async move {
973 const PRUNE_INTERVAL: Duration = Duration::from_hours(1);
974 const RETENTION_AFTER_EXPIRY: Duration = Duration::from_hours(24);
978
979 let mut interval = tokio::time::interval(PRUNE_INTERVAL);
980 loop {
981 interval.tick().await;
982
983 let cutoff_secs = duration_since_epoch()
984 .saturating_sub(RETENTION_AFTER_EXPIRY)
985 .as_secs();
986
987 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
988 let num_pruned = dbtx.prune_registered_incoming_contracts(cutoff_secs).await;
989 match dbtx.commit_tx_result().await {
990 Ok(()) => {
991 if num_pruned > 0 {
992 info!(
993 target: LOG_GATEWAY,
994 num_pruned,
995 "Pruned expired incoming contract records"
996 );
997 }
998 }
999 Err(err) => {
1000 warn!(
1001 target: LOG_GATEWAY,
1002 err = %err.fmt_compact(),
1003 "Failed to prune expired incoming contract records"
1004 );
1005 }
1006 }
1007 }
1008 },
1009 );
1010 }
1011
1012 pub async fn backup_all_federations(&self, dbtx: &mut DatabaseTransaction<'_, Committable>) {
1016 const BACKUP_THRESHOLD_DURATION: Duration = Duration::from_hours(24);
1019
1020 let now = fedimint_core::time::now();
1021 let threshold = now
1022 .checked_sub(BACKUP_THRESHOLD_DURATION)
1023 .expect("Cannot be negative");
1024 for (id, last_backup) in dbtx.load_backup_records().await {
1025 match last_backup {
1026 Some(backup_time) if backup_time < threshold => {
1027 let fed_manager = self.federation_manager.read().await;
1028 fed_manager.backup_federation(&id, dbtx, now).await;
1029 }
1030 None => {
1031 let fed_manager = self.federation_manager.read().await;
1032 fed_manager.backup_federation(&id, dbtx, now).await;
1033 }
1034 _ => {}
1035 }
1036 }
1037 }
1038
1039 fn start_gateway(
1042 &self,
1043 runtime: Arc<tokio::runtime::Runtime>,
1044 mut mnemonic_receiver: tokio::sync::broadcast::Receiver<MnemonicSource>,
1045 ) {
1046 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 60;
1047
1048 let self_copy = self.clone();
1049 let tg = self.task_group.clone();
1050 self.task_group.spawn(
1051 "Subscribe to intercepted lightning payments in stream",
1052 |handle| async move {
1053 let mut mnemonic_source = self_copy.mnemonic_source;
1054 loop {
1056 if handle.is_shutting_down() {
1057 info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
1058 break;
1059 }
1060
1061 if let GatewayState::NotConfigured{ .. } = self_copy.get_state().await {
1062 info!(
1063 target: LOG_GATEWAY,
1064 "Waiting for the mnemonic to be set before starting lightning receive loop."
1065 );
1066 info!(
1067 target: LOG_GATEWAY,
1068 "You might need to provide it from the UI or refer to documentation w.r.t how to initialize it."
1069 );
1070
1071 if let Ok(source) = mnemonic_receiver.recv().await {
1072 mnemonic_source = source;
1073 }
1074 info!(
1075 target: LOG_GATEWAY,
1076 ?mnemonic_source,
1077 "Received mnemonic, attempting to start lightning receive loop"
1078 );
1079 }
1080
1081 let payment_stream_task_group = tg.make_subgroup();
1082 let lnrpc_route = self_copy.create_lightning_client(runtime.clone(), mnemonic_source).await;
1083
1084 debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
1085 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
1086 {
1087 Ok((stream, ln_client)) => (stream, ln_client),
1088 Err(err) => {
1089 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
1090 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
1098 crit!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Lightning payment stream task group shutdown");
1099 }
1100 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
1101 continue
1102 }
1103 };
1104
1105 self_copy.set_gateway_state(GatewayState::Connected).await;
1107 info!(target: LOG_GATEWAY, "Established lightning payment stream");
1108
1109 let route_payments_response =
1110 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
1111
1112 self_copy.set_gateway_state(GatewayState::Disconnected).await;
1113 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
1114 crit!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Lightning payment stream task group shutdown");
1115 }
1116
1117 self_copy.unannounce_from_all_federations().await;
1118
1119 match route_payments_response {
1120 ReceivePaymentStreamAction::RetryAfterDelay => {
1121 warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
1122 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
1123 }
1124 ReceivePaymentStreamAction::NoRetry => break,
1125 }
1126 }
1127 },
1128 );
1129 }
1130
1131 async fn route_lightning_payments<'a>(
1135 &'a self,
1136 handle: &TaskHandle,
1137 mut stream: RouteHtlcStream<'a>,
1138 ln_client: Arc<dyn ILnRpcClient>,
1139 ) -> ReceivePaymentStreamAction {
1140 let LightningInfo::Connected {
1141 public_key: lightning_public_key,
1142 alias: lightning_alias,
1143 network: lightning_network,
1144 block_height: _,
1145 synced_to_chain,
1146 } = ln_client.parsed_node_info().await
1147 else {
1148 warn!(target: LOG_GATEWAY, "Failed to retrieve Lightning info");
1149 return ReceivePaymentStreamAction::RetryAfterDelay;
1150 };
1151
1152 assert!(
1153 self.network == lightning_network,
1154 "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
1155 self.network
1156 );
1157
1158 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
1159 info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
1160 } else {
1161 self.set_gateway_state(GatewayState::Syncing).await;
1162 info!(target: LOG_GATEWAY, "Waiting for chain sync");
1163 if let Err(err) = ln_client.wait_for_chain_sync().await {
1164 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
1165 return ReceivePaymentStreamAction::RetryAfterDelay;
1166 }
1167 }
1168
1169 let lightning_context = LightningContext {
1170 lnrpc: LnRpcTracked::new(ln_client, "gateway"),
1171 lightning_public_key,
1172 lightning_alias,
1173 lightning_network,
1174 };
1175 if let GatewayState::ShuttingDown { .. } = self
1176 .set_gateway_state(GatewayState::Running { lightning_context })
1177 .await
1178 {
1179 info!(
1180 target: LOG_GATEWAY,
1181 "Reconnected to the lightning node while shutting down, not accepting payments"
1182 );
1183 } else {
1184 info!(target: LOG_GATEWAY, "Gateway is running");
1185 }
1186
1187 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1188 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1191 let all_federations_configs =
1192 dbtx.load_federation_configs().await.into_iter().collect();
1193 self.register_federations(&all_federations_configs, &self.task_group)
1194 .await;
1195 }
1196
1197 let htlc_task_group = self.task_group.make_subgroup();
1200 if handle
1201 .cancel_on_shutdown(async move {
1202 loop {
1203 let payment_request_or = tokio::select! {
1204 payment_request_or = stream.next() => {
1205 payment_request_or
1206 }
1207 () = self.is_shutting_down_safely() => {
1208 break;
1209 }
1210 };
1211
1212 let Some(payment_request) = payment_request_or else {
1213 warn!(
1214 target: LOG_GATEWAY,
1215 "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
1216 );
1217 break;
1218 };
1219
1220 let state_guard = self.state.read().await;
1221 if let GatewayState::Running { ref lightning_context } = *state_guard {
1222 let gateway = self.clone();
1224 let lightning_context = lightning_context.clone();
1225 htlc_task_group.spawn_cancellable_silent(
1226 "handle_lightning_payment",
1227 async move {
1228 let start = fedimint_core::time::now();
1229 let outcome = gateway
1230 .handle_lightning_payment(payment_request, &lightning_context)
1231 .await;
1232 metrics::HTLC_HANDLING_DURATION_SECONDS
1233 .with_label_values(&[outcome])
1234 .observe(
1235 fedimint_core::time::now()
1236 .duration_since(start)
1237 .unwrap_or_default()
1238 .as_secs_f64(),
1239 );
1240 },
1241 );
1242 } else {
1243 warn!(
1244 target: LOG_GATEWAY,
1245 state = %state_guard,
1246 "Gateway isn't in a running state, cannot handle incoming payments."
1247 );
1248 break;
1249 }
1250 }
1251 })
1252 .await
1253 .is_ok()
1254 {
1255 warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
1256 ReceivePaymentStreamAction::RetryAfterDelay
1257 } else {
1258 info!(target: LOG_GATEWAY, "Received shutdown signal");
1259 ReceivePaymentStreamAction::NoRetry
1260 }
1261 }
1262
1263 async fn is_shutting_down_safely(&self) {
1266 loop {
1267 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
1268 return;
1269 }
1270
1271 fedimint_core::task::sleep(Duration::from_secs(1)).await;
1272 }
1273 }
1274
1275 pub async fn handle_lightning_payment(
1286 &self,
1287 payment_request: InterceptPaymentRequest,
1288 lightning_context: &LightningContext,
1289 ) -> &'static str {
1290 info!(
1291 target: LOG_GATEWAY,
1292 lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
1293 "Intercepting lightning payment",
1294 );
1295
1296 let _hold_invoice_guard = if payment_request.incoming_circuit().is_none() {
1307 let Some(guard) = self
1308 .lnv2_hold_invoice_lock_pool
1309 .try_lock(payment_request.payment_hash)
1310 else {
1311 info!(
1312 target: LOG_GATEWAY,
1313 payment_hash = %payment_request.payment_hash,
1314 "Dropping duplicate delivery of a HOLD invoice that is already being handled",
1315 );
1316 return "duplicate";
1317 };
1318 Some(guard)
1319 } else {
1320 None
1321 };
1322
1323 let lnv2_start = fedimint_core::time::now();
1324 let lnv2_result = self
1325 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
1326 .await;
1327 let lnv2_outcome = if lnv2_result.is_ok() {
1328 "success"
1329 } else {
1330 "error"
1331 };
1332 metrics::HTLC_LNV2_ATTEMPT_DURATION_SECONDS
1333 .with_label_values(&[lnv2_outcome])
1334 .observe(
1335 fedimint_core::time::now()
1336 .duration_since(lnv2_start)
1337 .unwrap_or_default()
1338 .as_secs_f64(),
1339 );
1340 if lnv2_result.is_ok() {
1341 return "lnv2";
1342 }
1343
1344 if payment_request.incoming_circuit().is_none()
1349 && let Err(PublicGatewayError::LNv2(err @ LNv2Error::IncomingAmountMismatch { .. })) =
1350 &lnv2_result
1351 {
1352 warn!(
1353 target: LOG_GATEWAY,
1354 payment_hash = %payment_request.payment_hash,
1355 err = %err.fmt_compact(),
1356 "Ignoring payment for a registered LNv2 contract whose amount does not match",
1357 );
1358 return "ignore";
1359 }
1360
1361 let lnv1_start = fedimint_core::time::now();
1362 let lnv1_result = self
1363 .try_handle_lightning_payment_ln_legacy(&payment_request, lightning_context)
1364 .await;
1365 let lnv1_outcome = if lnv1_result.is_ok() {
1366 "success"
1367 } else {
1368 "error"
1369 };
1370 metrics::HTLC_LNV1_ATTEMPT_DURATION_SECONDS
1371 .with_label_values(&[lnv1_outcome])
1372 .observe(
1373 fedimint_core::time::now()
1374 .duration_since(lnv1_start)
1375 .unwrap_or_default()
1376 .as_secs_f64(),
1377 );
1378 if lnv1_result.is_ok() {
1379 return "lnv1";
1380 }
1381
1382 let is_federation_scid = match payment_request.short_channel_id {
1387 Some(scid) => self
1388 .federation_manager
1389 .read()
1390 .await
1391 .get_client_for_index(scid)
1392 .is_some(),
1393 None => false,
1394 };
1395
1396 if is_federation_scid {
1397 warn!(
1404 target: LOG_GATEWAY,
1405 payment_hash = %payment_request.payment_hash,
1406 short_channel_id = ?payment_request.short_channel_id,
1407 amount_msat = payment_request.amount_msat,
1408 incoming_chan_id = payment_request.incoming_chan_id,
1409 htlc_id = payment_request.htlc_id,
1410 lnv2_err = ?lnv2_result.as_ref().err(),
1411 lnv1_err = ?lnv1_result.as_ref().err(),
1412 "Lightning payment for a served federation could not be accepted: cancelling HTLC",
1413 );
1414 Self::cancel_unmatched_lightning_payment(payment_request, lightning_context).await;
1415 "cancel"
1416 } else {
1417 Self::forward_lightning_payment(payment_request, lightning_context).await;
1422 "forward"
1423 }
1424 }
1425
1426 async fn try_handle_lightning_payment_lnv2(
1429 &self,
1430 htlc_request: &InterceptPaymentRequest,
1431 lightning_context: &LightningContext,
1432 ) -> Result<()> {
1433 let (contract, client) = self
1443 .get_registered_incoming_contract_and_client_v2(
1444 PaymentImage::Hash(htlc_request.payment_hash),
1445 htlc_request.incoming_amount_msat,
1446 )
1447 .await?;
1448
1449 let current_block_height = lightning_context
1452 .lnrpc
1453 .info()
1454 .await
1455 .ok()
1456 .map(|info| info.block_height);
1457 let blocks_to_claim_deadline =
1458 htlc_request.lnv2_blocks_to_claim_deadline(current_block_height);
1459
1460 if let Err(err) = client
1461 .get_first_module::<GatewayClientModuleV2>()
1462 .expect("Must have client module")
1463 .relay_incoming_htlc(
1464 htlc_request.payment_hash,
1465 htlc_request.incoming_chan_id,
1466 htlc_request.htlc_id,
1467 contract,
1468 htlc_request.incoming_amount_msat,
1469 blocks_to_claim_deadline,
1470 )
1471 .await
1472 {
1473 warn!(
1474 target: LOG_GATEWAY,
1475 err = %err.fmt_compact(),
1476 "Error relaying incoming lightning payment"
1477 );
1478
1479 let outcome = InterceptPaymentResponse {
1480 action: PaymentAction::Cancel,
1481 payment_hash: htlc_request.payment_hash,
1482 incoming_chan_id: htlc_request.incoming_chan_id,
1483 htlc_id: htlc_request.htlc_id,
1484 };
1485
1486 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1487 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
1488 }
1489 }
1490
1491 Ok(())
1492 }
1493
1494 async fn try_handle_lightning_payment_ln_legacy(
1497 &self,
1498 htlc_request: &InterceptPaymentRequest,
1499 lightning_context: &LightningContext,
1500 ) -> Result<()> {
1501 let Some(federation_index) = htlc_request.short_channel_id else {
1503 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1504 "Incoming payment has not last hop short channel id".to_string(),
1505 )));
1506 };
1507
1508 let Some(client) = self
1509 .federation_manager
1510 .read()
1511 .await
1512 .get_client_for_index(federation_index)
1513 else {
1514 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())));
1515 };
1516
1517 client
1522 .borrow()
1523 .with(|client| async {
1524 let htlc = Htlc::from(htlc_request.clone());
1525 let lnv1 = client
1526 .get_first_module::<GatewayClientModule>()
1527 .map_err(|_| {
1528 PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1529 "Federation does not have LNv1 module".to_string(),
1530 ))
1531 })?;
1532 match lnv1
1533 .gateway_handle_intercepted_htlc(htlc, async {
1534 Ok(lightning_context.lnrpc.info().await?.block_height)
1535 })
1536 .await
1537 {
1538 Ok(_) => Ok(()),
1539 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1540 format!("Error intercepting lightning payment {}", e.fmt_compact()),
1541 ))),
1542 }
1543 })
1544 .await
1545 }
1546
1547 async fn cancel_unmatched_lightning_payment(
1561 htlc_request: InterceptPaymentRequest,
1562 lightning_context: &LightningContext,
1563 ) {
1564 let outcome = InterceptPaymentResponse {
1565 action: PaymentAction::Cancel,
1566 payment_hash: htlc_request.payment_hash,
1567 incoming_chan_id: htlc_request.incoming_chan_id,
1568 htlc_id: htlc_request.htlc_id,
1569 };
1570
1571 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1572 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1573 }
1574 }
1575
1576 async fn forward_lightning_payment(
1581 htlc_request: InterceptPaymentRequest,
1582 lightning_context: &LightningContext,
1583 ) {
1584 let outcome = InterceptPaymentResponse {
1585 action: PaymentAction::Forward,
1586 payment_hash: htlc_request.payment_hash,
1587 incoming_chan_id: htlc_request.incoming_chan_id,
1588 htlc_id: htlc_request.htlc_id,
1589 };
1590
1591 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1592 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1593 }
1594 }
1595
1596 async fn set_gateway_state(&self, state: GatewayState) -> GatewayState {
1607 let mut lock = self.state.write().await;
1608
1609 if let GatewayState::ShuttingDown { .. } = *lock {
1610 match state {
1611 GatewayState::Running { lightning_context } => {
1612 *lock = GatewayState::ShuttingDown { lightning_context };
1613 }
1614 ignored => {
1615 info!(
1616 target: LOG_GATEWAY,
1617 ignored_state = %ignored,
1618 "Gateway is shutting down, ignoring state change"
1619 );
1620 }
1621 }
1622 } else {
1623 *lock = state;
1624 }
1625
1626 lock.clone()
1627 }
1628
1629 #[doc(hidden)]
1637 pub async fn set_gateway_state_out_of_band(&self, state: GatewayState) {
1638 self.set_gateway_state(state).await;
1639 }
1640
1641 pub async fn handle_get_federation_config(
1644 &self,
1645 federation_id_or: Option<FederationId>,
1646 ) -> AdminResult<GatewayFedConfig> {
1647 if !matches!(self.get_state().await, GatewayState::Running { .. }) {
1648 return Ok(GatewayFedConfig {
1649 federations: BTreeMap::new(),
1650 });
1651 }
1652
1653 let federations = if let Some(federation_id) = federation_id_or {
1654 let mut federations = BTreeMap::new();
1655 federations.insert(
1656 federation_id,
1657 self.federation_manager
1658 .read()
1659 .await
1660 .get_federation_config(federation_id)
1661 .await?,
1662 );
1663 federations
1664 } else {
1665 self.federation_manager
1666 .read()
1667 .await
1668 .get_all_federation_configs()
1669 .await
1670 };
1671
1672 Ok(GatewayFedConfig { federations })
1673 }
1674
1675 pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
1678 let client = self.select_client(payload.federation_id).await?;
1679
1680 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1681 let address = wallet_module
1682 .allocate_deposit_address_expert_only(())
1683 .await
1684 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?
1685 .address;
1686 Ok(address)
1687 } else if let Ok(wallet_module) = client
1688 .value()
1689 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1690 {
1691 Ok(wallet_module.receive().await)
1692 } else {
1693 Err(AdminGatewayError::Unexpected(anyhow!(
1694 "No wallet module found"
1695 )))
1696 }
1697 }
1698
1699 async fn handle_pay_invoice_msg(
1702 &self,
1703 payload: fedimint_ln_client::pay::PayInvoicePayload,
1704 ) -> Result<Preimage> {
1705 let GatewayState::Running { .. } = self.get_state().await else {
1706 return Err(PublicGatewayError::Lightning(
1707 LightningRpcError::FailedToConnect,
1708 ));
1709 };
1710
1711 debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1712 let client = self.select_client(payload.federation_id).await?;
1713 let contract_id = payload.contract_id;
1714 let gateway_module = &client
1715 .value()
1716 .get_first_module::<GatewayClientModule>()
1717 .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1718 .map_err(PublicGatewayError::LNv1)?;
1719 let operation_id = gateway_module
1720 .gateway_pay_bolt11_invoice(payload)
1721 .await
1722 .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1723 .map_err(PublicGatewayError::LNv1)?;
1724 let mut updates = gateway_module
1725 .gateway_subscribe_ln_pay(operation_id)
1726 .await
1727 .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1728 .map_err(PublicGatewayError::LNv1)?
1729 .into_stream();
1730 while let Some(update) = updates.next().await {
1731 match update {
1732 GatewayExtPayStates::Success { preimage, .. } => {
1733 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1734 return Ok(preimage);
1735 }
1736 GatewayExtPayStates::Fail {
1737 error,
1738 error_message,
1739 } => {
1740 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1741 error: Box::new(error),
1742 message: format!(
1743 "{error_message} while paying invoice with contract id {contract_id}"
1744 ),
1745 }));
1746 }
1747 GatewayExtPayStates::Canceled { error } => {
1748 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1749 error: Box::new(error.clone()),
1750 message: format!(
1751 "Cancelled with {error} while paying invoice with contract id {contract_id}"
1752 ),
1753 }));
1754 }
1755 GatewayExtPayStates::Created => {
1756 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1757 }
1758 other => {
1759 debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1760 }
1761 }
1762 }
1763
1764 Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1765 anyhow!("Ran out of state updates while paying invoice"),
1766 )))
1767 }
1768
1769 pub async fn handle_backup_msg(
1772 &self,
1773 BackupPayload { federation_id }: BackupPayload,
1774 ) -> AdminResult<()> {
1775 let federation_manager = self.federation_manager.read().await;
1776 let client = federation_manager
1777 .client(&federation_id)
1778 .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1779 format!("Gateway has not connected to {federation_id}")
1780 )))?
1781 .value();
1782 let metadata: BTreeMap<String, String> = BTreeMap::new();
1783 #[allow(deprecated)]
1784 client
1785 .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1786 metadata,
1787 ))
1788 .await
1789 .map_err(anyhow::Error::from)?;
1790 Ok(())
1791 }
1792
1793 pub async fn handle_recheck_address_msg(
1795 &self,
1796 payload: DepositAddressRecheckPayload,
1797 ) -> AdminResult<()> {
1798 let client = self.select_client(payload.federation_id).await?;
1799
1800 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1801 wallet_module
1802 .recheck_pegin_address_by_address(payload.address)
1803 .await
1804 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
1805 Ok(())
1806 } else if client
1807 .value()
1808 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1809 .is_ok()
1810 {
1811 Ok(())
1813 } else {
1814 Err(AdminGatewayError::Unexpected(anyhow!(
1815 "No wallet module found"
1816 )))
1817 }
1818 }
1819
1820 pub async fn handle_receive_ecash_msg(
1822 &self,
1823 payload: ReceiveEcashPayload,
1824 ) -> Result<ReceiveEcashResponse> {
1825 let federation_id_prefix = base32::decode_prefixed::<fedimint_mintv2_client::ECash>(
1827 FEDIMINT_PREFIX,
1828 &payload.notes,
1829 )
1830 .ok()
1831 .and_then(|e| e.mint())
1832 .map(|id| id.to_prefix())
1833 .or_else(|| {
1834 OOBNotes::from_str(&payload.notes)
1835 .ok()
1836 .map(|n| n.federation_id_prefix())
1837 })
1838 .ok_or_else(|| PublicGatewayError::ReceiveEcashError {
1839 failure_reason: "Invalid ecash format: could not parse as ECash or OOBNotes"
1840 .to_string(),
1841 })?;
1842
1843 let client = self
1844 .federation_manager
1845 .read()
1846 .await
1847 .get_client_for_federation_id_prefix(federation_id_prefix)
1848 .ok_or(FederationNotConnected {
1849 federation_id_prefix,
1850 })?;
1851
1852 if let Ok(mint) = client.value().get_first_module::<MintClientModule>() {
1854 let notes = OOBNotes::from_str(&payload.notes).map_err(|e| {
1855 PublicGatewayError::ReceiveEcashError {
1856 failure_reason: format!(
1857 "Expected OOBNotes for MintV1 federation: {}",
1858 e.fmt_compact()
1859 ),
1860 }
1861 })?;
1862 let amount = notes.total_amount();
1863
1864 let operation_id = mint.reissue_external_notes(notes, ()).await.map_err(|e| {
1865 PublicGatewayError::ReceiveEcashError {
1866 failure_reason: e.fmt_compact().to_string(),
1867 }
1868 })?;
1869
1870 let mut updates = mint
1871 .subscribe_reissue_external_notes(operation_id)
1872 .await
1873 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1874 failure_reason: format!(
1875 "Could not subscribe to reissue operation: {}",
1876 e.fmt_compact()
1877 ),
1878 })?
1879 .into_stream();
1880
1881 let mut reissued = false;
1886 while let Some(update) = updates.next().await {
1887 match update {
1888 ReissueExternalNotesState::Failed(failure_reason) => {
1889 return Err(PublicGatewayError::ReceiveEcashError { failure_reason });
1890 }
1891 ReissueExternalNotesState::Done => reissued = true,
1892 ReissueExternalNotesState::Created | ReissueExternalNotesState::Issuing => {}
1893 }
1894 }
1895
1896 if !reissued {
1897 return Err(PublicGatewayError::ReceiveEcashError {
1898 failure_reason: "Reissue operation ended before the notes were reissued"
1899 .to_string(),
1900 });
1901 }
1902
1903 Ok(ReceiveEcashResponse { amount })
1904 } else if let Ok(mint) = client
1905 .value()
1906 .get_primary_module_for_unit::<MintV2ClientModule>(
1907 fedimint_core::module::AmountUnit::BITCOIN,
1908 )
1909 {
1910 let ecash: fedimint_mintv2_client::ECash =
1911 base32::decode_prefixed(FEDIMINT_PREFIX, &payload.notes).map_err(|e| {
1912 PublicGatewayError::ReceiveEcashError {
1913 failure_reason: format!("Expected ECash for MintV2 federation: {e}"),
1914 }
1915 })?;
1916 let amount = ecash.amount();
1917
1918 let operation_id = mint
1919 .receive(ecash, serde_json::Value::Null)
1920 .await
1921 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1922 failure_reason: e.fmt_compact().to_string(),
1923 })?;
1924
1925 let final_state = mint
1926 .await_final_receive_operation_state(operation_id)
1927 .await
1928 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1929 failure_reason: e.fmt_compact().to_string(),
1930 })?;
1931 match final_state {
1932 fedimint_mintv2_client::FinalReceiveOperationState::Success => {}
1933 fedimint_mintv2_client::FinalReceiveOperationState::Rejected => {
1934 return Err(PublicGatewayError::ReceiveEcashError {
1935 failure_reason: "ECash receive was rejected".to_string(),
1936 });
1937 }
1938 }
1939
1940 Ok(ReceiveEcashResponse { amount })
1941 } else {
1942 Err(PublicGatewayError::ReceiveEcashError {
1943 failure_reason: "No mint module found".to_string(),
1944 })
1945 }
1946 }
1947
1948 pub async fn handle_get_invoice_msg(
1951 &self,
1952 payload: GetInvoiceRequest,
1953 ) -> AdminResult<Option<GetInvoiceResponse>> {
1954 let lightning_context = self.get_lightning_context().await?;
1955 let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1956 Ok(invoice)
1957 }
1958
1959 pub async fn handle_withdraw_to_onchain_msg(
1962 &self,
1963 payload: WithdrawToOnchainPayload,
1964 ) -> AdminResult<WithdrawResponse> {
1965 let address = self.handle_get_ln_onchain_address_msg().await?;
1966 let withdraw = WithdrawPayload {
1967 address: address.into_unchecked(),
1968 federation_id: payload.federation_id,
1969 amount: payload.amount,
1970 quoted_fees: None,
1971 };
1972 self.handle_withdraw_msg(withdraw).await
1973 }
1974
1975 pub async fn handle_pegin_from_onchain_msg(
1978 &self,
1979 payload: PeginFromOnchainPayload,
1980 ) -> AdminResult<Txid> {
1981 let deposit = DepositAddressPayload {
1982 federation_id: payload.federation_id,
1983 };
1984 let address = self.handle_address_msg(deposit).await?;
1985 let send_onchain = SendOnchainRequest {
1986 address: address.into_unchecked(),
1987 amount: payload.amount,
1988 fee_rate_sats_per_vbyte: payload.fee_rate_sats_per_vbyte,
1989 };
1990 let txid = self.handle_send_onchain_msg(send_onchain).await?;
1991
1992 Ok(txid)
1993 }
1994
1995 async fn register_federations(
2003 &self,
2004 federations: &BTreeMap<FederationId, FederationConfig>,
2005 register_task_group: &TaskGroup,
2006 ) {
2007 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
2008 info!(
2009 target: LOG_GATEWAY,
2010 "Gateway is shutting down, skipping federation registration"
2011 );
2012 return;
2013 }
2014
2015 if let Ok(lightning_context) = self.get_lightning_context().await {
2016 let route_hints = lightning_context
2017 .lnrpc
2018 .parsed_route_hints(self.num_route_hints)
2019 .await;
2020 if route_hints.is_empty() {
2021 warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
2022 }
2023
2024 for (federation_id, federation_config) in federations {
2025 let routing_fees = match RoutingFees::try_from(federation_config.lightning_fee) {
2030 Ok(routing_fees) => routing_fees,
2031 Err(err) => {
2032 warn!(
2033 target: LOG_GATEWAY,
2034 %federation_id,
2035 err = %err.fmt_compact(),
2036 "Skipping registration, the configured lightning fee cannot be announced. Set a smaller fee with `set_fees`."
2037 );
2038 continue;
2039 }
2040 };
2041
2042 let fed_manager = self.federation_manager.read().await;
2043 if let Some(client) = fed_manager.client(federation_id) {
2044 let federation_id = *federation_id;
2045 let client_arc = client.clone().into_value();
2046 let route_hints = route_hints.clone();
2047 let lightning_context = lightning_context.clone();
2048 let registration_health = self.registration_health.clone();
2049 let registrations = self
2050 .registrations
2051 .clone()
2052 .into_iter()
2053 .map(|(protocol, registration)| {
2054 let attempt =
2055 registration_health.begin_lnv1_attempt(federation_id, protocol);
2056 (registration, attempt)
2057 })
2058 .collect::<Vec<_>>();
2059
2060 register_task_group.spawn_cancellable_silent(
2061 "register federation",
2062 async move {
2063 let Ok(gateway_client) =
2064 client_arc.get_first_module::<GatewayClientModule>()
2065 else {
2066 return;
2067 };
2068
2069 for (registration, attempt) in registrations {
2070 let succeeded = gateway_client
2071 .try_register_with_federation(
2072 route_hints.clone(),
2073 GW_ANNOUNCEMENT_TTL,
2074 routing_fees,
2075 lightning_context.clone(),
2076 registration.endpoint_url,
2077 registration.keypair,
2078 )
2079 .await;
2080 registration_health
2081 .complete_attempt(
2082 attempt,
2083 succeeded,
2084 fedimint_core::time::now(),
2085 fedimint_core::runtime::Instant::now(),
2086 )
2087 .await;
2088 }
2089 },
2090 );
2091 }
2092 }
2093 }
2094 }
2095
2096 pub async fn select_client(
2099 &self,
2100 federation_id: FederationId,
2101 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
2102 {
2103 self.federation_manager
2104 .read()
2105 .await
2106 .client(&federation_id)
2107 .cloned()
2108 .ok_or(FederationNotConnected {
2109 federation_id_prefix: federation_id.to_prefix(),
2110 })
2111 }
2112
2113 async fn load_mnemonic(gateway_db: &Database) -> Option<Mnemonic> {
2114 let secret = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db)
2115 .await
2116 .ok()?;
2117 Mnemonic::from_entropy(&secret).ok()
2118 }
2119
2120 async fn load_clients(&self) -> AdminResult<()> {
2124 if let GatewayState::NotConfigured { .. } = self.get_state().await {
2125 return Ok(());
2126 }
2127
2128 let mut federation_manager = self.federation_manager.write().await;
2129
2130 let configs = {
2131 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2132 dbtx.load_federation_configs().await
2133 };
2134
2135 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
2136 federation_manager.set_next_index(max_federation_index + 1);
2137 }
2138
2139 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2140 .await
2141 .expect("mnemonic should be set");
2142
2143 for (federation_id, config) in configs {
2144 let federation_index = config.federation_index;
2145 match Box::pin(Spanned::try_new(
2146 info_span!(target: LOG_GATEWAY, "client", federation_id = %federation_id.clone()),
2147 self.client_builder
2148 .build(config, Arc::new(self.clone()), &mnemonic),
2149 ))
2150 .await
2151 {
2152 Ok(client) => {
2153 federation_manager.add_client(federation_index, client);
2154 }
2155 _ => {
2156 warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
2157 }
2158 }
2159 }
2160
2161 Ok(())
2162 }
2163
2164 fn register_clients_timer(&self) {
2170 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2172 info!(target: LOG_GATEWAY, "Spawning register task...");
2173 let gateway = self.clone();
2174 let register_task_group = self.task_group.make_subgroup();
2175 self.task_group.spawn_cancellable("register clients", async move {
2176 loop {
2177 let gateway_state = gateway.get_state().await;
2178 if let GatewayState::Running { .. } = &gateway_state {
2179 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
2180 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
2181 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
2182 } else {
2183 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
2185 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");
2186 sleep(NOT_RUNNING_RETRY).await;
2187 continue;
2188 }
2189
2190 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
2193 }
2194 });
2195 }
2196 }
2197
2198 async fn check_federation_network(
2201 client: &ClientHandleArc,
2202 network: Network,
2203 ) -> AdminResult<()> {
2204 let federation_id = client.federation_id();
2205 let config = client.config().await;
2206
2207 let lnv1_cfg = config
2208 .modules
2209 .values()
2210 .find(|m| LightningCommonInit::KIND == m.kind);
2211
2212 let lnv2_cfg = config
2213 .modules
2214 .values()
2215 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
2216
2217 if lnv1_cfg.is_none() && lnv2_cfg.is_none() {
2219 return Err(AdminGatewayError::ClientCreationError(anyhow!(
2220 "Federation {federation_id} does not have any lightning module (LNv1 or LNv2)"
2221 )));
2222 }
2223
2224 if let Some(cfg) = lnv1_cfg {
2226 let ln_cfg: &LightningClientConfig = cfg.cast().map_err(anyhow::Error::from)?;
2227
2228 if ln_cfg.network.0 != network {
2229 crit!(
2230 target: LOG_GATEWAY,
2231 federation_id = %federation_id,
2232 network = %network,
2233 "Incorrect LNv1 network for federation",
2234 );
2235 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2236 "Unsupported LNv1 network {}",
2237 ln_cfg.network
2238 ))));
2239 }
2240 }
2241
2242 if let Some(cfg) = lnv2_cfg {
2244 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig =
2245 cfg.cast().map_err(anyhow::Error::from)?;
2246
2247 if ln_cfg.network != network {
2248 crit!(
2249 target: LOG_GATEWAY,
2250 federation_id = %federation_id,
2251 network = %network,
2252 "Incorrect LNv2 network for federation",
2253 );
2254 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2255 "Unsupported LNv2 network {}",
2256 ln_cfg.network
2257 ))));
2258 }
2259 }
2260
2261 Ok(())
2262 }
2263
2264 pub async fn get_lightning_context(
2274 &self,
2275 ) -> std::result::Result<LightningContext, LightningRpcError> {
2276 match self.get_state().await {
2277 GatewayState::Running { lightning_context }
2278 | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
2279 _ => Err(LightningRpcError::FailedToConnect),
2280 }
2281 }
2282
2283 async fn await_lightning_context(&self) -> LightningContext {
2304 loop {
2305 match self.get_lightning_context().await {
2306 Ok(lightning_context) => return lightning_context,
2307 Err(err) => {
2308 let state = self.get_state().await;
2309
2310 warn!(
2311 target: LOG_GATEWAY,
2312 err = %err.fmt_compact(),
2313 %state,
2314 retry_interval_secs = LIGHTNING_CONTEXT_RETRY_INTERVAL.as_secs(),
2315 "Not connected to the lightning node, waiting before asking it again",
2316 );
2317
2318 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
2319 }
2320 }
2321 }
2322 }
2323
2324 pub async fn unannounce_from_all_federations(&self) {
2327 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2328 for registration in self.registrations.values() {
2329 self.federation_manager
2330 .read()
2331 .await
2332 .unannounce_from_all_federations(registration.keypair)
2333 .await;
2334 }
2335 }
2336 }
2337
2338 async fn create_lightning_client(
2339 &self,
2340 runtime: Arc<tokio::runtime::Runtime>,
2341 mnemonic_source: MnemonicSource,
2342 ) -> Box<dyn ILnRpcClient> {
2343 match self.lightning_mode.clone() {
2344 LightningMode::Lnd {
2345 lnd_rpc_addr,
2346 lnd_tls_cert,
2347 lnd_macaroon,
2348 lnd_time_pref,
2349 lnd_payment_timeout_secs,
2350 } => {
2351 let gateway_db = self.gateway_db.clone();
2356 let lnv2_filter: Lnv2HoldInvoiceFilter = Arc::new(move |hash| {
2357 let gateway_db = gateway_db.clone();
2358 Box::pin(async move {
2359 gateway_db
2360 .begin_transaction_nc()
2361 .await
2362 .load_registered_incoming_contract(PaymentImage::Hash(hash))
2363 .await
2364 .is_some()
2365 })
2366 });
2367
2368 Box::new(GatewayLndClient::new(
2369 lnd_rpc_addr,
2370 lnd_tls_cert,
2371 lnd_macaroon,
2372 lnd_time_pref,
2373 lnd_payment_timeout_secs,
2374 None,
2375 lnv2_filter,
2376 ))
2377 }
2378 LightningMode::Ldk {
2379 lightning_port,
2380 alias,
2381 } => {
2382 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2383 .await
2384 .expect("mnemonic should be set");
2385 retry("create LDK Node", fibonacci_max_one_hour(), || async {
2389 ldk::GatewayLdkClient::new(
2390 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
2391 self.chain_source.clone(),
2392 self.network,
2393 lightning_port,
2394 alias.clone(),
2395 mnemonic.clone(),
2396 mnemonic_source,
2397 runtime.clone(),
2398 )
2399 .map(Box::new)
2400 .map_err(anyhow::Error::from)
2403 })
2404 .await
2405 .expect("Could not create LDK Node")
2406 }
2407 }
2408 }
2409}
2410
2411#[async_trait]
2412impl IAdminGateway for Gateway {
2413 type Error = AdminGatewayError;
2414
2415 async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
2418 let GatewayState::Running { lightning_context } = self.get_state().await else {
2419 return Ok(GatewayInfo {
2420 federations: vec![],
2421 federation_fake_scids: None,
2422 version_hash: fedimint_build_code_version_env!().to_string(),
2423 gateway_state: self.state.read().await.to_string(),
2424 lightning_info: LightningInfo::NotConnected,
2425 lightning_mode: self.lightning_mode.clone(),
2426 registrations: self
2427 .registrations
2428 .iter()
2429 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2430 .collect(),
2431 });
2432 };
2433
2434 let dbtx = self.gateway_db.begin_transaction_nc().await;
2435 let federations = self
2436 .federation_manager
2437 .read()
2438 .await
2439 .federation_info_all_federations(dbtx)
2440 .await;
2441
2442 let channels: BTreeMap<u64, FederationId> = federations
2443 .iter()
2444 .map(|federation_info| {
2445 (
2446 federation_info.config.federation_index,
2447 federation_info.federation_id,
2448 )
2449 })
2450 .collect();
2451
2452 let lightning_info = lightning_context.lnrpc.parsed_node_info().await;
2453
2454 Ok(GatewayInfo {
2455 federations,
2456 federation_fake_scids: Some(channels),
2457 version_hash: fedimint_build_code_version_env!().to_string(),
2458 gateway_state: self.state.read().await.to_string(),
2459 lightning_info,
2460 lightning_mode: self.lightning_mode.clone(),
2461 registrations: self
2462 .registrations
2463 .iter()
2464 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2465 .collect(),
2466 })
2467 }
2468
2469 async fn handle_list_channels_msg(
2472 &self,
2473 ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
2474 let context = self.get_lightning_context().await?;
2475 let response = context.lnrpc.list_channels().await?;
2476 Ok(response.channels)
2477 }
2478
2479 async fn handle_payment_summary_msg(
2482 &self,
2483 PaymentSummaryPayload {
2484 start_millis,
2485 end_millis,
2486 }: PaymentSummaryPayload,
2487 ) -> AdminResult<PaymentSummaryResponse> {
2488 let federation_manager = self.federation_manager.read().await;
2489 let fed_configs = federation_manager.get_all_federation_configs().await;
2490 let federation_ids = fed_configs.keys().collect::<Vec<_>>();
2491 let start = UNIX_EPOCH + Duration::from_millis(start_millis);
2492 let end = UNIX_EPOCH + Duration::from_millis(end_millis);
2493
2494 if start > end {
2495 return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
2496 }
2497
2498 let mut outgoing = StructuredPaymentEvents::default();
2499 let mut incoming = StructuredPaymentEvents::default();
2500 for fed_id in federation_ids {
2501 let client = federation_manager
2502 .client(fed_id)
2503 .expect("No client available")
2504 .value();
2505 let all_events = &get_events_for_duration(client, start, end).await;
2506
2507 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
2508 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
2509 outgoing.combine(&mut lnv1_outgoing);
2510 incoming.combine(&mut lnv1_incoming);
2511 outgoing.combine(&mut lnv2_outgoing);
2512 incoming.combine(&mut lnv2_incoming);
2513 }
2514
2515 Ok(PaymentSummaryResponse {
2516 outgoing: PaymentStats::compute(&outgoing),
2517 incoming: PaymentStats::compute(&incoming),
2518 })
2519 }
2520
2521 async fn handle_leave_federation(
2526 &self,
2527 payload: LeaveFedPayload,
2528 ) -> AdminResult<FederationInfo> {
2529 let mut federation_manager = self.federation_manager.write().await;
2532 let mut dbtx = self.gateway_db.begin_transaction().await;
2533
2534 let federation_info = federation_manager
2535 .leave_federation(
2536 payload.federation_id,
2537 &mut dbtx.to_ref_nc(),
2538 self.registrations.values().collect(),
2539 )
2540 .await?;
2541
2542 dbtx.remove_federation_config(payload.federation_id).await;
2543 dbtx.commit_tx().await;
2544 self.registration_health
2545 .clear_federation(payload.federation_id)
2546 .await;
2547 Ok(federation_info)
2548 }
2549
2550 async fn handle_connect_federation(
2555 &self,
2556 payload: ConnectFedPayload,
2557 ) -> AdminResult<FederationInfo> {
2558 let GatewayState::Running { lightning_context } = self.get_state().await else {
2559 return Err(AdminGatewayError::Lightning(
2560 LightningRpcError::FailedToConnect,
2561 ));
2562 };
2563
2564 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
2565 AdminGatewayError::ClientCreationError(anyhow!(format!(
2566 "Invalid federation member string {e:?}"
2567 )))
2568 })?;
2569
2570 let federation_id = invite_code.federation_id();
2571
2572 let mut federation_manager = self.federation_manager.write().await;
2573
2574 if federation_manager.has_federation(federation_id) {
2576 return Err(AdminGatewayError::ClientCreationError(anyhow!(
2577 "Federation has already been registered"
2578 )));
2579 }
2580
2581 let federation_index = federation_manager.pop_next_index()?;
2584
2585 let federation_config = FederationConfig {
2586 invite_code,
2587 federation_index,
2588 lightning_fee: self.default_routing_fees,
2589 transaction_fee: self.default_transaction_fees,
2590 payment_policies: BTreeSet::new(),
2591 _connector: ConnectorType::Tcp,
2593 };
2594
2595 let routing_fees = RoutingFees::try_from(federation_config.lightning_fee)
2599 .map_err(|err| AdminGatewayError::GatewayConfigurationError(err.to_string()))?;
2600
2601 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2602 .await
2603 .expect("mnemonic should be set");
2604 let recover = payload.recover.unwrap_or(false);
2605 if recover {
2606 self.client_builder
2607 .recover(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2608 .await?;
2609 }
2610
2611 let client = self
2612 .client_builder
2613 .build(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2614 .await?;
2615
2616 if recover {
2617 client.wait_for_all_active_state_machines().await;
2618 }
2619
2620 let federation_info = FederationInfo {
2623 federation_id,
2624 federation_name: federation_manager.federation_name(&client).await,
2625 balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
2626 warn!(
2627 target: LOG_GATEWAY,
2628 err = %err.fmt_compact(),
2629 %federation_id,
2630 "Balance not immediately available after joining/recovering."
2631 );
2632 Amount::default()
2633 }),
2634 config: federation_config.clone(),
2635 last_backup_time: None,
2636 };
2637
2638 Self::check_federation_network(&client, self.network).await?;
2639 if matches!(self.lightning_mode, LightningMode::Lnd { .. })
2640 && let Ok(lnv1) = client.get_first_module::<GatewayClientModule>()
2641 {
2642 for (protocol, registration) in &self.registrations {
2643 let attempt = self
2644 .registration_health
2645 .begin_lnv1_attempt(federation_id, protocol.clone());
2646 let succeeded = lnv1
2647 .try_register_with_federation(
2648 Vec::new(),
2650 GW_ANNOUNCEMENT_TTL,
2651 routing_fees,
2652 lightning_context.clone(),
2653 registration.endpoint_url.clone(),
2654 registration.keypair,
2655 )
2656 .await;
2657 self.registration_health
2658 .complete_attempt(
2659 attempt,
2660 succeeded,
2661 fedimint_core::time::now(),
2662 fedimint_core::runtime::Instant::now(),
2663 )
2664 .await;
2665 }
2666 }
2667
2668 federation_manager.add_client(
2670 federation_index,
2671 Spanned::new(
2672 info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
2673 async { client },
2674 )
2675 .await,
2676 );
2677
2678 let mut dbtx = self.gateway_db.begin_transaction().await;
2679 dbtx.save_federation_config(&federation_config).await;
2680 dbtx.save_federation_backup_record(federation_id, None)
2681 .await;
2682 dbtx.commit_tx().await;
2683 debug!(
2684 target: LOG_GATEWAY,
2685 federation_id = %federation_id,
2686 federation_index = %federation_index,
2687 "Federation connected"
2688 );
2689
2690 Ok(federation_info)
2691 }
2692
2693 async fn handle_set_fees_msg(
2696 &self,
2697 SetFeesPayload {
2698 federation_id,
2699 lightning_base,
2700 lightning_parts_per_million,
2701 transaction_base,
2702 transaction_parts_per_million,
2703 }: SetFeesPayload,
2704 ) -> AdminResult<()> {
2705 let mut dbtx = self.gateway_db.begin_transaction().await;
2706 let mut fed_configs = if let Some(fed_id) = federation_id {
2707 dbtx.load_federation_configs()
2708 .await
2709 .into_iter()
2710 .filter(|(id, _)| *id == fed_id)
2711 .collect::<BTreeMap<_, _>>()
2712 } else {
2713 dbtx.load_federation_configs().await
2714 };
2715
2716 if let Some(fed_id) = federation_id
2719 && fed_configs.is_empty()
2720 {
2721 return Err(FederationNotConnected {
2722 federation_id_prefix: fed_id.to_prefix(),
2723 }
2724 .into());
2725 }
2726
2727 let federation_manager = self.federation_manager.read().await;
2728
2729 for (federation_id, config) in &mut fed_configs {
2730 let mut lightning_fee = config.lightning_fee;
2731 if let Some(lightning_base) = lightning_base {
2732 lightning_fee.base = lightning_base;
2733 }
2734
2735 if let Some(lightning_ppm) = lightning_parts_per_million {
2736 lightning_fee.parts_per_million = lightning_ppm;
2737 }
2738
2739 let mut transaction_fee = config.transaction_fee;
2740 if let Some(transaction_base) = transaction_base {
2741 transaction_fee.base = transaction_base;
2742 }
2743
2744 if let Some(transaction_ppm) = transaction_parts_per_million {
2745 transaction_fee.parts_per_million = transaction_ppm;
2746 }
2747
2748 federation_manager
2751 .client(federation_id)
2752 .ok_or(FederationNotConnected {
2753 federation_id_prefix: federation_id.to_prefix(),
2754 })?;
2755
2756 let send_fees = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
2762 AdminGatewayError::GatewayConfigurationError(format!(
2763 "Total Send fees overflowed, they may not exceed {}",
2764 PaymentFee::SEND_FEE_LIMIT
2765 ))
2766 })?;
2767
2768 if !send_fees.is_within(&PaymentFee::SEND_FEE_LIMIT) {
2770 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2771 "Total Send fees exceeded {}",
2772 PaymentFee::SEND_FEE_LIMIT
2773 )));
2774 }
2775
2776 if !transaction_fee.is_within(&PaymentFee::RECEIVE_FEE_LIMIT) {
2778 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2779 "Transaction fees exceeded RECEIVE LIMIT {}",
2780 PaymentFee::RECEIVE_FEE_LIMIT
2781 )));
2782 }
2783
2784 config.lightning_fee = lightning_fee;
2785 config.transaction_fee = transaction_fee;
2786 dbtx.save_federation_config(config).await;
2787 }
2788
2789 dbtx.commit_tx().await;
2790
2791 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2792 let register_task_group = TaskGroup::new();
2793
2794 self.register_federations(&fed_configs, ®ister_task_group)
2795 .await;
2796 }
2797
2798 Ok(())
2799 }
2800
2801 async fn handle_set_payment_policy_msg(
2806 &self,
2807 SetPaymentPolicyPayload {
2808 federation_id,
2809 receive_enabled,
2810 }: SetPaymentPolicyPayload,
2811 ) -> AdminResult<()> {
2812 let _policy_change = self.payment_policy_lock.write().await;
2816
2817 let mut dbtx = self.gateway_db.begin_transaction().await;
2818 let mut fed_configs = if let Some(fed_id) = federation_id {
2819 dbtx.load_federation_configs()
2820 .await
2821 .into_iter()
2822 .filter(|(id, _)| *id == fed_id)
2823 .collect::<BTreeMap<_, _>>()
2824 } else {
2825 dbtx.load_federation_configs().await
2826 };
2827
2828 if let Some(fed_id) = federation_id
2831 && fed_configs.is_empty()
2832 {
2833 return Err(FederationNotConnected {
2834 federation_id_prefix: fed_id.to_prefix(),
2835 }
2836 .into());
2837 }
2838
2839 let federation_manager = self.federation_manager.read().await;
2840
2841 for (federation_id, config) in &mut fed_configs {
2842 federation_manager
2843 .client(federation_id)
2844 .ok_or(FederationNotConnected {
2845 federation_id_prefix: federation_id.to_prefix(),
2846 })?;
2847
2848 if let Some(receive_enabled) = receive_enabled {
2849 if receive_enabled {
2850 config
2851 .payment_policies
2852 .remove(&PaymentPolicy::ReceivesDisabled);
2853 } else {
2854 config
2855 .payment_policies
2856 .insert(PaymentPolicy::ReceivesDisabled);
2857 }
2858 }
2859
2860 info!(
2861 target: LOG_GATEWAY,
2862 %federation_id,
2863 payment_policies = ?config.payment_policies,
2864 "Updated federation payment policy"
2865 );
2866
2867 dbtx.save_federation_config(config).await;
2868 }
2869
2870 dbtx.commit_tx().await;
2871
2872 Ok(())
2873 }
2874
2875 async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
2879 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2880 .await
2881 .expect("mnemonic should be set");
2882 let words = mnemonic
2883 .words()
2884 .map(std::string::ToString::to_string)
2885 .collect::<Vec<_>>();
2886 let all_federations = self
2887 .federation_manager
2888 .read()
2889 .await
2890 .get_all_federation_configs()
2891 .await
2892 .keys()
2893 .copied()
2894 .collect::<BTreeSet<_>>();
2895 let legacy_federations = self.client_builder.legacy_federations(all_federations);
2896 let mnemonic_response = MnemonicResponse {
2897 mnemonic: words,
2898 legacy_federations,
2899 };
2900 Ok(mnemonic_response)
2901 }
2902
2903 async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
2906 info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
2907 let context = self.get_lightning_context().await?;
2908 let res = context.lnrpc.open_channel(payload).await?;
2909 info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
2910 Txid::from_str(&res.funding_txid).map_err(|e| {
2911 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2912 failure_reason: format!("Received invalid channel funding txid string {e}"),
2913 })
2914 })
2915 }
2916
2917 async fn handle_connect_peer_msg(&self, payload: ConnectPeerRequest) -> AdminResult<()> {
2920 info!(
2921 target: LOG_GATEWAY,
2922 pubkey = %payload.node_address.pubkey,
2923 host = %payload.node_address.host_with_port(),
2924 "Connecting to Lightning peer..."
2925 );
2926 let context = self.get_lightning_context().await?;
2927 context.lnrpc.connect_peer(payload).await?;
2928 info!(target: LOG_GATEWAY, "Connected to Lightning peer");
2929 Ok(())
2930 }
2931
2932 async fn handle_close_channels_with_peer_msg(
2935 &self,
2936 payload: CloseChannelsWithPeerRequest,
2937 ) -> AdminResult<CloseChannelsWithPeerResponse> {
2938 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Closing lightning channel...");
2939 let context = self.get_lightning_context().await?;
2940 let response = context
2941 .lnrpc
2942 .close_channels_with_peer(payload.clone())
2943 .await?;
2944 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Initiated channel closure");
2945 Ok(response)
2946 }
2947
2948 async fn handle_set_channel_fees_msg(&self, payload: SetChannelFeesRequest) -> AdminResult<()> {
2951 info!(
2952 target: LOG_GATEWAY,
2953 funding_outpoint = %payload.funding_outpoint,
2954 base_fee_msat = payload.base_fee_msat,
2955 parts_per_million = payload.parts_per_million,
2956 "Updating channel fees..."
2957 );
2958 let context = self.get_lightning_context().await?;
2959 context.lnrpc.set_channel_fees(payload).await?;
2960 Ok(())
2961 }
2962
2963 async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
2966 let dbtx = self.gateway_db.begin_transaction_nc().await;
2967 let federation_infos = self
2968 .federation_manager
2969 .read()
2970 .await
2971 .federation_info_all_federations(dbtx)
2972 .await;
2973
2974 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
2975 .iter()
2976 .map(|federation_info| FederationBalanceInfo {
2977 federation_id: federation_info.federation_id,
2978 ecash_balance_msats: Amount {
2979 msats: federation_info.balance_msat.msats,
2980 },
2981 })
2982 .collect();
2983
2984 let context = self.get_lightning_context().await?;
2985 let lightning_node_balances = context.lnrpc.get_balances().await?;
2986
2987 Ok(GatewayBalances {
2988 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
2989 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
2990 ecash_balances,
2991 inbound_lightning_liquidity_msats: lightning_node_balances
2992 .inbound_lightning_liquidity_msats,
2993 })
2994 }
2995
2996 async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
2998 let context = self.get_lightning_context().await?;
2999 let response = context.lnrpc.send_onchain(payload.clone()).await?;
3000 let txid =
3001 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
3002 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
3003 })?;
3004 info!(onchain_request = %payload, txid = %txid, "Sent onchain transaction");
3005 Ok(txid)
3006 }
3007
3008 async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
3010 let context = self.get_lightning_context().await?;
3011 let response = context.lnrpc.get_ln_onchain_address().await?;
3012
3013 let address = Address::from_str(&response.address).map_err(|e| {
3014 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
3015 failure_reason: e.to_string(),
3016 })
3017 })?;
3018
3019 address.require_network(self.network).map_err(|e| {
3020 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
3021 failure_reason: e.to_string(),
3022 })
3023 })
3024 }
3025
3026 async fn handle_deposit_address_msg(
3027 &self,
3028 payload: DepositAddressPayload,
3029 ) -> AdminResult<Address> {
3030 self.handle_address_msg(payload).await
3031 }
3032
3033 async fn handle_receive_ecash_msg(
3034 &self,
3035 payload: ReceiveEcashPayload,
3036 ) -> AdminResult<ReceiveEcashResponse> {
3037 Self::handle_receive_ecash_msg(self, payload)
3038 .await
3039 .map_err(|e| AdminGatewayError::Unexpected(anyhow::anyhow!("{e}")))
3040 }
3041
3042 async fn handle_create_invoice_for_operator_msg(
3045 &self,
3046 payload: CreateInvoiceForOperatorPayload,
3047 ) -> AdminResult<Bolt11Invoice> {
3048 let GatewayState::Running { lightning_context } = self.get_state().await else {
3049 return Err(AdminGatewayError::Lightning(
3050 LightningRpcError::FailedToConnect,
3051 ));
3052 };
3053
3054 Bolt11Invoice::from_str(
3055 &lightning_context
3056 .lnrpc
3057 .create_invoice(CreateInvoiceRequest {
3058 payment_hash: None, amount_msat: payload.amount_msats,
3061 expiry_secs: payload.expiry_secs.unwrap_or(3600),
3062 description: payload.description.map(InvoiceDescription::Direct),
3063 })
3064 .await?
3065 .invoice,
3066 )
3067 .map_err(|e| {
3068 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
3069 failure_reason: e.to_string(),
3070 })
3071 })
3072 }
3073
3074 async fn handle_pay_invoice_for_operator_msg(
3077 &self,
3078 payload: PayInvoiceForOperatorPayload,
3079 ) -> AdminResult<Preimage> {
3080 const BASE_FEE: u64 = 50;
3082 const FEE_DENOMINATOR: u64 = 100;
3083 const MAX_DELAY: u64 = 1008;
3084
3085 let GatewayState::Running { lightning_context } = self.get_state().await else {
3086 return Err(AdminGatewayError::Lightning(
3087 LightningRpcError::FailedToConnect,
3088 ));
3089 };
3090
3091 let max_fee = BASE_FEE
3092 + payload
3093 .invoice
3094 .amount_milli_satoshis()
3095 .context("Invoice is missing amount")?
3096 .saturating_div(FEE_DENOMINATOR);
3097
3098 let res = lightning_context
3099 .lnrpc
3100 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
3101 .await?;
3102 Ok(res.preimage)
3103 }
3104
3105 async fn handle_list_transactions_msg(
3107 &self,
3108 payload: ListTransactionsPayload,
3109 ) -> AdminResult<ListTransactionsResponse> {
3110 let lightning_context = self.get_lightning_context().await?;
3111 let response = lightning_context
3112 .lnrpc
3113 .list_transactions(payload.start_secs, payload.end_secs)
3114 .await?;
3115 Ok(response)
3116 }
3117
3118 async fn handle_spend_ecash_msg(
3120 &self,
3121 payload: SpendEcashPayload,
3122 ) -> AdminResult<SpendEcashResponse> {
3123 let client = self
3124 .select_client(payload.federation_id)
3125 .await?
3126 .into_value();
3127
3128 if let Ok(mint_module) = client.get_first_module::<MintClientModule>() {
3129 let notes = mint_module
3130 .send_oob_notes(payload.amount, ())
3131 .await
3132 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
3133 debug!(target: LOG_GATEWAY, ?notes, "Spend ecash notes");
3134 Ok(SpendEcashResponse {
3135 notes: notes.to_string(),
3136 })
3137 } else if let Ok(mint_module) = client.get_primary_module_for_unit::<MintV2ClientModule>(
3138 fedimint_core::module::AmountUnit::BITCOIN,
3139 ) {
3140 let (_, ecash) = mint_module
3141 .send(payload.amount, serde_json::Value::Null, true)
3142 .await
3143 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
3144
3145 Ok(SpendEcashResponse {
3146 notes: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
3147 })
3148 } else {
3149 Err(AdminGatewayError::Unexpected(anyhow::anyhow!(
3150 "No mint module available"
3151 )))
3152 }
3153 }
3154
3155 async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
3158 let was_running = {
3162 let mut state_guard = self.state.write().await;
3163 if let GatewayState::Running { lightning_context } = state_guard.clone() {
3164 *state_guard = GatewayState::ShuttingDown { lightning_context };
3165 true
3166 } else {
3167 false
3168 }
3169 };
3170
3171 if was_running {
3179 self.federation_manager
3180 .read()
3181 .await
3182 .wait_for_incoming_payments()
3183 .await?;
3184 }
3185
3186 let tg = task_group.clone();
3187 tg.spawn("Kill Gateway", |_task_handle| async {
3188 if let Err(err) = task_group.shutdown_join_all(Duration::from_mins(3)).await {
3189 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error shutting down gateway");
3190 }
3191 });
3192 Ok(())
3193 }
3194
3195 fn get_task_group(&self) -> TaskGroup {
3196 self.task_group.clone()
3197 }
3198
3199 async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> AdminResult<WithdrawResponse> {
3202 let WithdrawPayload {
3203 amount,
3204 address,
3205 federation_id,
3206 quoted_fees,
3207 } = payload;
3208
3209 let address_network = get_network_for_address(&address);
3210 let gateway_network = self.network;
3211 let Ok(address) = address.require_network(gateway_network) else {
3212 return Err(AdminGatewayError::WithdrawError {
3213 failure_reason: format!(
3214 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
3215 ),
3216 });
3217 };
3218
3219 let client = self.select_client(federation_id).await?;
3220
3221 if let Ok(wallet_module) = client
3222 .value()
3223 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
3224 {
3225 let quoted_fee = quoted_fees.map(|fees| fees.amount());
3228 return withdraw_v2(client.value(), &wallet_module, &address, amount, quoted_fee).await;
3229 }
3230
3231 let wallet_module = client
3232 .value()
3233 .get_first_module::<WalletClientModule>()
3234 .map_err(anyhow::Error::from)?;
3235
3236 let (withdraw_amount, fees) = match quoted_fees {
3239 Some(fees) => {
3241 let amt = match amount {
3242 BitcoinAmountOrAll::Amount(a) => a,
3243 BitcoinAmountOrAll::All => {
3244 return Err(AdminGatewayError::WithdrawError {
3246 failure_reason:
3247 "Cannot use 'all' with quoted fees - amount must be resolved first"
3248 .to_string(),
3249 });
3250 }
3251 };
3252 (amt, fees)
3253 }
3254 None => match amount {
3256 BitcoinAmountOrAll::All => {
3261 let balance = client.value().get_balance_for_btc().await.map_err(|err| {
3262 AdminGatewayError::Unexpected(anyhow!(
3263 "Balance not available: {}",
3264 err.fmt_compact()
3265 ))
3266 })?;
3267
3268 wallet_module
3269 .max_withdrawable_amount(&address, balance)
3270 .await
3271 .map_err(|err| AdminGatewayError::WithdrawError {
3272 failure_reason: format!(
3273 "Insufficient funds. Balance: {balance}: {}",
3274 err.fmt_compact()
3275 ),
3276 })?
3277 }
3278 BitcoinAmountOrAll::Amount(amount) => (
3279 amount,
3280 wallet_module
3281 .get_withdraw_fees(&address, amount)
3282 .await
3283 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?,
3284 ),
3285 },
3286 };
3287
3288 let operation_id = wallet_module
3289 .withdraw(&address, withdraw_amount, fees, ())
3290 .await
3291 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
3292 let mut updates = wallet_module
3293 .subscribe_withdraw_updates(operation_id)
3294 .await
3295 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?
3296 .into_stream();
3297
3298 while let Some(update) = updates.next().await {
3299 match update {
3300 WithdrawState::Succeeded(txid) => {
3301 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds");
3302 return Ok(WithdrawResponse { txid, fees });
3303 }
3304 WithdrawState::Failed(e) => {
3305 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
3306 }
3307 WithdrawState::Created => {}
3308 }
3309 }
3310
3311 Err(AdminGatewayError::WithdrawError {
3312 failure_reason: "Ran out of state updates while withdrawing".to_string(),
3313 })
3314 }
3315
3316 async fn handle_withdraw_preview_msg(
3319 &self,
3320 payload: WithdrawPreviewPayload,
3321 ) -> AdminResult<WithdrawPreviewResponse> {
3322 if let BitcoinAmountOrAll::Amount(amount) = payload.amount
3325 && amount > bitcoin::Amount::MAX_MONEY
3326 {
3327 return Err(AdminGatewayError::WithdrawError {
3328 failure_reason: "Withdrawal amount exceeds maximum Bitcoin supply".to_string(),
3329 });
3330 }
3331
3332 let gateway_network = self.network;
3333 let address_checked = payload
3334 .address
3335 .clone()
3336 .require_network(gateway_network)
3337 .map_err(|_| AdminGatewayError::WithdrawError {
3338 failure_reason: "Address network mismatch".to_string(),
3339 })?;
3340
3341 let client = self.select_client(payload.federation_id).await?;
3342
3343 let WithdrawDetails {
3344 amount,
3345 mint_fees,
3346 peg_out_fees,
3347 flat_peg_out_fee,
3348 } = match payload.amount {
3349 BitcoinAmountOrAll::All => {
3350 calculate_max_withdrawable(client.value(), &address_checked).await?
3351 }
3352 BitcoinAmountOrAll::Amount(btc_amount) => {
3353 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
3354 WithdrawDetails {
3355 amount: btc_amount.into(),
3356 mint_fees: None,
3357 peg_out_fees: wallet_module
3358 .get_withdraw_fees(&address_checked, btc_amount)
3359 .await
3360 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?,
3361 flat_peg_out_fee: false,
3362 }
3363 } else if let Ok(wallet_module) = client
3364 .value()
3365 .get_first_module::<fedimint_walletv2_client::WalletClientModule>(
3366 ) {
3367 let fee = wallet_module.send_fee().await.map_err(|e| {
3368 AdminGatewayError::WithdrawError {
3369 failure_reason: e.fmt_compact().to_string(),
3370 }
3371 })?;
3372 let output_amount = btc_amount.checked_add(fee).ok_or_else(|| {
3375 AdminGatewayError::WithdrawError {
3376 failure_reason: "Withdrawal amount overflow".to_string(),
3377 }
3378 })?;
3379 let federation_fees = wallet_module
3380 .send_fee_quote(output_amount)
3381 .await
3382 .map_err(|e| AdminGatewayError::WithdrawError {
3383 failure_reason: e.fmt_compact().to_string(),
3384 })?
3385 .total()
3386 .get_bitcoin();
3387 WithdrawDetails {
3388 amount: btc_amount.into(),
3389 mint_fees: Some(federation_fees),
3390 peg_out_fees: PegOutFees::from_amount(fee),
3391 flat_peg_out_fee: true,
3392 }
3393 } else {
3394 return Err(AdminGatewayError::Unexpected(anyhow!(
3395 "No wallet module found"
3396 )));
3397 }
3398 }
3399 };
3400
3401 let total_cost = amount
3402 .checked_add(peg_out_fees.amount().into())
3403 .and_then(|a| a.checked_add(mint_fees.unwrap_or(Amount::ZERO)))
3404 .ok_or_else(|| AdminGatewayError::Unexpected(anyhow!("Total cost overflow")))?;
3405
3406 Ok(WithdrawPreviewResponse {
3407 withdraw_amount: amount,
3408 address: payload.address.assume_checked().to_string(),
3409 peg_out_fees,
3410 total_cost,
3411 mint_fees,
3412 flat_peg_out_fee,
3413 })
3414 }
3415
3416 async fn handle_payment_log_msg(
3428 &self,
3429 PaymentLogPayload {
3430 end_position,
3431 pagination_size,
3432 federation_id,
3433 event_kinds,
3434 }: PaymentLogPayload,
3435 ) -> AdminResult<PaymentLogResponse> {
3436 const BATCH_SIZE: u64 = 10_000;
3437 let federation_manager = self.federation_manager.read().await;
3438 let client = federation_manager
3439 .client(&federation_id)
3440 .ok_or(FederationNotConnected {
3441 federation_id_prefix: federation_id.to_prefix(),
3442 })?
3443 .value();
3444
3445 let event_kinds = if event_kinds.is_empty() {
3449 ALL_GATEWAY_EVENTS.to_vec()
3450 } else {
3451 event_kinds
3452 };
3453
3454 let end_position = if let Some(position) = end_position {
3455 position
3456 } else {
3457 let mut dbtx = client.db().begin_transaction_nc().await;
3458 dbtx.get_next_event_log_id().await
3459 };
3460
3461 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
3462
3463 let mut payment_log = Vec::new();
3464
3465 while payment_log.len() < pagination_size {
3466 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
3467 let mut filtered_batch = batch
3468 .into_iter()
3469 .filter(|e| e.id() <= end_position && event_kinds.contains(&e.as_raw().kind))
3470 .collect::<Vec<_>>();
3471 filtered_batch.reverse();
3472 payment_log.extend(filtered_batch);
3473
3474 start_position = start_position.saturating_sub(BATCH_SIZE);
3476
3477 if start_position == EventLogId::LOG_START {
3478 break;
3479 }
3480 }
3481
3482 payment_log.truncate(pagination_size);
3484
3485 Ok(PaymentLogResponse(payment_log))
3486 }
3487
3488 async fn handle_set_mnemonic_msg(&self, payload: SetMnemonicPayload) -> AdminResult<()> {
3491 let mut state_guard = self.state.write().await;
3496
3497 let GatewayState::NotConfigured { mnemonic_sender } = state_guard.clone() else {
3499 return Err(AdminGatewayError::MnemonicError(anyhow!(
3500 "Gateway is not is NotConfigured state"
3501 )));
3502 };
3503
3504 let (mnemonic, mnemonic_source) = if let Some(words) = payload.words {
3505 info!(target: LOG_GATEWAY, "Using user provided mnemonic");
3506 let mnemonic = Mnemonic::parse_in_normalized(Language::English, words.as_str())
3507 .map_err(|e| {
3508 AdminGatewayError::MnemonicError(anyhow!(format!(
3509 "Seed phrase provided in environment was invalid {e:?}"
3510 )))
3511 })?;
3512 (mnemonic, MnemonicSource::Existing)
3513 } else {
3514 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
3515 (
3516 Bip39RootSecretStrategy::<12>::random(&mut OsRng),
3517 MnemonicSource::Generated,
3518 )
3519 };
3520
3521 Client::store_encodable_client_secret(&self.gateway_db, mnemonic.to_entropy())
3522 .await
3523 .map_err(|err| AdminGatewayError::MnemonicError(err.into()))?;
3524
3525 *state_guard = GatewayState::Disconnected;
3526 drop(state_guard);
3527
3528 let _ = mnemonic_sender.send(mnemonic_source);
3530
3531 Ok(())
3532 }
3533
3534 async fn handle_create_offer_for_operator_msg(
3536 &self,
3537 payload: CreateOfferPayload,
3538 ) -> AdminResult<CreateOfferResponse> {
3539 let lightning_context = self.get_lightning_context().await?;
3540 let offer = lightning_context.lnrpc.create_offer(
3541 payload.amount,
3542 payload.description,
3543 payload.expiry_secs,
3544 payload.quantity,
3545 )?;
3546 Ok(CreateOfferResponse { offer })
3547 }
3548
3549 async fn handle_pay_offer_for_operator_msg(
3551 &self,
3552 payload: PayOfferPayload,
3553 ) -> AdminResult<PayOfferResponse> {
3554 let lightning_context = self.get_lightning_context().await?;
3555 let preimage = lightning_context
3556 .lnrpc
3557 .pay_offer(
3558 payload.offer,
3559 payload.quantity,
3560 payload.amount,
3561 payload.payer_note,
3562 )
3563 .await?;
3564 Ok(PayOfferResponse {
3565 preimage: preimage.to_string(),
3566 })
3567 }
3568
3569 async fn handle_export_invite_codes(
3572 &self,
3573 ) -> BTreeMap<FederationId, BTreeMap<PeerId, (String, InviteCode)>> {
3574 let fed_manager = self.federation_manager.read().await;
3575 fed_manager.all_invite_codes().await
3576 }
3577
3578 async fn handle_get_note_summary_msg(
3581 &self,
3582 federation_id: &FederationId,
3583 ) -> AdminResult<TieredCounts> {
3584 let fed_manager = self.federation_manager.read().await;
3585 fed_manager.get_note_summary(federation_id).await
3586 }
3587
3588 fn get_password_hash(&self) -> String {
3589 self.bcrypt_password_hash.clone()
3590 }
3591
3592 fn gatewayd_version(&self) -> String {
3593 let gatewayd_version = env!("CARGO_PKG_VERSION");
3594 gatewayd_version.to_string()
3595 }
3596
3597 async fn get_chain_source(&self) -> (ChainSource, Network) {
3598 (self.chain_source.clone(), self.network)
3599 }
3600
3601 fn lightning_mode(&self) -> LightningMode {
3602 self.lightning_mode.clone()
3603 }
3604
3605 async fn is_configured(&self) -> bool {
3606 !matches!(self.get_state().await, GatewayState::NotConfigured { .. })
3607 }
3608}
3609
3610impl Gateway {
3612 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
3616 self.federation_manager
3617 .read()
3618 .await
3619 .client(federation_id)
3620 .and_then(|client| {
3621 client
3624 .value()
3625 .get_first_module::<GatewayClientModuleV2>()
3626 .ok()
3627 .map(|module| module.keypair.public_key())
3628 })
3629 }
3630
3631 async fn receive_enabled(&self, federation_id: FederationId) -> bool {
3636 self.gateway_db
3637 .begin_transaction_nc()
3638 .await
3639 .load_federation_config(federation_id)
3640 .await
3641 .is_none_or(|config| config.receive_enabled())
3642 }
3643
3644 async fn admit_fresh_receive(&self, federation_id: FederationId) -> Option<PaymentPolicyGuard> {
3648 let guard = self.payment_policy_lock.clone().read_owned().await;
3651
3652 self.receive_enabled(federation_id)
3653 .await
3654 .then(|| PaymentPolicyGuard::new(guard))
3655 }
3656
3657 pub async fn routing_info_v2(
3660 &self,
3661 federation_id: &FederationId,
3662 ) -> Result<Option<RoutingInfo>> {
3663 let context = self.get_lightning_context().await?;
3664
3665 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
3666 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
3667 PublicGatewayError::FederationNotConnected(FederationNotConnected {
3668 federation_id_prefix: federation_id.to_prefix(),
3669 }),
3670 )?;
3671
3672 let lightning_fee = fed_config.lightning_fee;
3673 let transaction_fee = fed_config.transaction_fee;
3674 let receive_enabled = fed_config.receive_enabled();
3675
3676 let send_fee_default = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
3679 PublicGatewayError::Unexpected(anyhow!(
3680 "The configured fees of federation {federation_id} cannot be added"
3681 ))
3682 })?;
3683
3684 Ok(self
3685 .public_key_v2(federation_id)
3686 .await
3687 .map(|module_public_key| RoutingInfo {
3688 lightning_public_key: context.lightning_public_key,
3689 lightning_alias: Some(context.lightning_alias.clone()),
3690 module_public_key,
3691 send_fee_default,
3692 send_fee_minimum: transaction_fee,
3696 expiration_delta_default: 1440,
3697 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
3698 receive_fee: transaction_fee,
3701 receive_enabled,
3702 }))
3703 }
3704
3705 pub async fn send_payment_v2(
3708 &self,
3709 payload: SendPaymentPayload,
3710 ) -> Result<std::result::Result<[u8; 32], Signature>> {
3711 let client = self.select_client(payload.federation_id).await?;
3712 let module = client
3715 .value()
3716 .get_first_module::<GatewayClientModuleV2>()
3717 .map_err(|err| PublicGatewayError::LNv2(LNv2Error::OutgoingPayment(err.into())))?;
3718
3719 module
3720 .send_payment(payload)
3721 .await
3722 .map_err(|err| LNv2Error::OutgoingPayment(err.into()))
3723 .map_err(PublicGatewayError::LNv2)
3724 }
3725
3726 pub async fn create_bolt11_invoice_v2(
3731 &self,
3732 payload: CreateBolt11InvoicePayload,
3733 ) -> Result<Bolt11Invoice> {
3734 if !self.invoice_rate_limiter.try_acquire() {
3738 return Err(PublicGatewayError::RateLimited);
3739 }
3740
3741 let contract = payload.contract.clone();
3743
3744 if !fedimint_core::runtime::spawn_blocking(move || contract.verify()).await {
3745 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3746 "The contract is invalid".to_string(),
3747 )));
3748 }
3749
3750 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
3751 LNv2Error::IncomingPayment(format!(
3752 "Federation {} does not exist",
3753 payload.federation_id
3754 )),
3755 )?;
3756
3757 if !payment_info.receive_enabled {
3758 return Err(PublicGatewayError::ReceiveDisabled {
3759 federation_id_prefix: payload.federation_id.to_prefix(),
3760 });
3761 }
3762
3763 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
3764 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3765 "The incoming contract is keyed to another gateway".to_string(),
3766 )));
3767 }
3768
3769 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
3770
3771 if contract_amount == Amount::ZERO {
3772 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3773 "Zero amount incoming contracts are not supported".to_string(),
3774 )));
3775 }
3776
3777 if contract_amount != payload.contract.commitment.amount {
3778 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3779 "The contract amount does not pay the correct amount of fees".to_string(),
3780 )));
3781 }
3782
3783 if payload.contract.commitment.expiration_or_fee <= duration_since_epoch().as_secs() {
3784 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3785 "The contract has already expired".to_string(),
3786 )));
3787 }
3788
3789 if payload.expiry_secs > MAX_INVOICE_EXPIRY_SECS {
3790 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3791 "The invoice expiry exceeds the maximum of one day".to_string(),
3792 )));
3793 }
3794
3795 let payment_hash = match payload.contract.commitment.payment_image {
3796 PaymentImage::Hash(payment_hash) => payment_hash,
3797 PaymentImage::Point(..) => {
3798 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3799 "PaymentImage is not a payment hash".to_string(),
3800 )));
3801 }
3802 };
3803
3804 let mut dbtx = self.gateway_db.begin_transaction().await;
3809
3810 let invoice_expires_at_secs = duration_since_epoch()
3811 .as_secs()
3812 .saturating_add(u64::from(payload.expiry_secs));
3813
3814 if dbtx
3815 .save_registered_incoming_contract(
3816 payload.federation_id,
3817 payload.amount,
3818 invoice_expires_at_secs,
3819 payload.contract,
3820 )
3821 .await
3822 .is_some()
3823 {
3824 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3825 "PaymentHash is already registered".to_string(),
3826 )));
3827 }
3828
3829 dbtx.commit_tx_result().await.map_err(|_| {
3830 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3831 "Payment hash is already registered".to_string(),
3832 ))
3833 })?;
3834
3835 match self
3836 .create_invoice_via_lnrpc_v2(
3837 payment_hash,
3838 payload.amount,
3839 payload.description.clone(),
3840 payload.expiry_secs,
3841 )
3842 .await
3843 {
3844 Ok(invoice) => Ok(invoice),
3845 Err(err) => {
3846 let mut dbtx = self.gateway_db.begin_transaction().await;
3849 dbtx.delete_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3850 .await;
3851 if let Err(db_err) = dbtx.commit_tx_result().await {
3852 warn!(
3853 target: LOG_GATEWAY,
3854 err = %db_err.fmt_compact(),
3855 %payment_hash,
3856 "Failed to release incoming contract reservation after Lightning error"
3857 );
3858 }
3859
3860 Err(err.into())
3861 }
3862 }
3863 }
3864
3865 pub async fn create_invoice_via_lnrpc_v2(
3868 &self,
3869 payment_hash: sha256::Hash,
3870 amount: Amount,
3871 description: Bolt11InvoiceDescription,
3872 expiry_time: u32,
3873 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
3874 let lnrpc = self.get_lightning_context().await?.lnrpc;
3875
3876 let response = match description {
3877 Bolt11InvoiceDescription::Direct(description) => {
3878 lnrpc
3879 .create_invoice(CreateInvoiceRequest {
3880 payment_hash: Some(payment_hash),
3881 amount_msat: amount.msats,
3882 expiry_secs: expiry_time,
3883 description: Some(InvoiceDescription::Direct(description)),
3884 })
3885 .await?
3886 }
3887 Bolt11InvoiceDescription::Hash(hash) => {
3888 lnrpc
3889 .create_invoice(CreateInvoiceRequest {
3890 payment_hash: Some(payment_hash),
3891 amount_msat: amount.msats,
3892 expiry_secs: expiry_time,
3893 description: Some(InvoiceDescription::Hash(hash)),
3894 })
3895 .await?
3896 }
3897 };
3898
3899 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
3900 LightningRpcError::FailedToGetInvoice {
3901 failure_reason: e.to_string(),
3902 }
3903 })
3904 }
3905
3906 pub async fn verify_bolt11_preimage_v2(
3907 &self,
3908 payment_hash: sha256::Hash,
3909 wait: bool,
3910 ) -> std::result::Result<VerifyResponse, String> {
3911 let registered_contract = self
3912 .gateway_db
3913 .begin_transaction_nc()
3914 .await
3915 .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3916 .await
3917 .ok_or("Unknown payment hash".to_string())?;
3918
3919 let client = self
3920 .select_client(registered_contract.federation_id)
3921 .await
3922 .map_err(|_| "Not connected to federation".to_string())?
3923 .into_value();
3924
3925 let operation_id = OperationId::from_encodable(®istered_contract.contract);
3926
3927 if !(wait || client.operation_exists(operation_id).await) {
3928 return Ok(VerifyResponse::pending());
3929 }
3930
3931 let module = client
3932 .get_first_module::<GatewayClientModuleV2>()
3933 .expect("Must have client module");
3934
3935 let Ok(state) = timeout(VERIFY_WAIT_TIMEOUT, module.await_receive(operation_id)).await
3936 else {
3937 return Ok(VerifyResponse::pending());
3938 };
3939
3940 let preimage = match state {
3941 FinalReceiveState::Success(preimage) => Ok(preimage),
3942 FinalReceiveState::Failure => Err("Payment has failed".to_string()),
3943 FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
3944 FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
3945 }?;
3946
3947 Ok(VerifyResponse::settled(preimage))
3948 }
3949
3950 pub async fn get_registered_incoming_contract_and_client_v2(
3954 &self,
3955 payment_image: PaymentImage,
3956 amount_msats: u64,
3957 ) -> Result<(IncomingContract, ClientHandleArc)> {
3958 let registered_incoming_contract = self
3959 .gateway_db
3960 .begin_transaction_nc()
3961 .await
3962 .load_registered_incoming_contract(payment_image)
3963 .await
3964 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3965 "No corresponding decryption contract available".to_string(),
3966 )))?;
3967
3968 if registered_incoming_contract.incoming_amount_msats != amount_msats {
3969 return Err(PublicGatewayError::LNv2(
3970 LNv2Error::IncomingAmountMismatch {
3971 registered_msats: registered_incoming_contract.incoming_amount_msats,
3972 payment_msats: amount_msats,
3973 },
3974 ));
3975 }
3976
3977 let client = self
3978 .select_client(registered_incoming_contract.federation_id)
3979 .await?
3980 .into_value();
3981
3982 Ok((registered_incoming_contract.contract, client))
3983 }
3984
3985 async fn await_complete_htlc(
3994 &self,
3995 htlc: InterceptPaymentResponse,
3996 ) -> std::result::Result<(), LightningRpcError> {
3997 loop {
3998 let lightning_context = self.await_lightning_context().await;
3999
4000 match lightning_context.lnrpc.complete_htlc(htlc.clone()).await {
4001 Ok(()) => return Ok(()),
4002 Err(err @ LightningRpcError::HtlcCompletionRejected { .. }) => {
4003 warn!(
4004 target: LOG_GATEWAY,
4005 err = %err.fmt_compact(),
4006 "Lightning cannot reach the requested terminal HTLC outcome",
4007 );
4008 return Err(err);
4009 }
4010 Err(err) => {
4011 warn!(
4012 target: LOG_GATEWAY,
4013 err = %err.fmt_compact(),
4014 "Failure trying to complete HTLC, retrying",
4015 );
4016 }
4017 }
4018
4019 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
4020 }
4021 }
4022
4023 async fn await_outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
4032 loop {
4033 let lightning_context = self.await_lightning_context().await;
4034
4035 match lightning_context
4036 .lnrpc
4037 .outbound_payment_exists(payment_hash)
4038 .await
4039 {
4040 Ok(exists) => return exists,
4041 Err(err) => {
4042 warn!(
4043 target: LOG_GATEWAY,
4044 err = %err.fmt_compact(),
4045 %payment_hash,
4046 "Failed to check for a dispatched payment, retrying",
4047 );
4048 }
4049 }
4050
4051 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
4052 }
4053 }
4054}
4055
4056#[async_trait]
4057impl IGatewayClientV2 for Gateway {
4058 async fn complete_htlc(
4059 &self,
4060 htlc_response: InterceptPaymentResponse,
4061 ) -> std::result::Result<(), LightningRpcError> {
4062 self.await_complete_htlc(htlc_response).await
4063 }
4064
4065 async fn is_direct_swap(
4066 &self,
4067 invoice: &Bolt11Invoice,
4068 ) -> std::result::Result<Option<(IncomingContract, ClientHandleArc)>, GatewayClientV2Error>
4069 {
4070 let lightning_context = self.await_lightning_context().await;
4075 if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
4076 let (contract, client) = self
4077 .get_registered_incoming_contract_and_client_v2(
4078 PaymentImage::Hash(*invoice.payment_hash()),
4079 invoice
4080 .amount_milli_satoshis()
4081 .expect("The amount invoice has been previously checked"),
4082 )
4083 .await
4084 .map_err(GatewayClientV2Error::new)?;
4085 Ok(Some((contract, client)))
4086 } else {
4087 Ok(None)
4088 }
4089 }
4090
4091 async fn pay(
4092 &self,
4093 invoice: Bolt11Invoice,
4094 max_delay: u64,
4095 max_fee: Amount,
4096 ) -> std::result::Result<[u8; 32], LightningRpcError> {
4097 let lightning_context = self.await_lightning_context().await;
4100 lightning_context
4101 .lnrpc
4102 .pay(invoice, max_delay, max_fee)
4103 .await
4104 .map(|response| response.preimage.0)
4105 }
4106
4107 async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
4108 self.await_outbound_payment_exists(payment_hash).await
4109 }
4110
4111 async fn begin_fresh_receive(
4112 &self,
4113 federation_id: &FederationId,
4114 ) -> Option<PaymentPolicyGuard> {
4115 self.admit_fresh_receive(*federation_id).await
4116 }
4117
4118 async fn min_contract_amount(
4119 &self,
4120 federation_id: &FederationId,
4121 amount: u64,
4122 ) -> std::result::Result<Amount, GatewayClientV2Error> {
4123 Ok(self
4124 .routing_info_v2(federation_id)
4125 .await
4126 .map_err(GatewayClientV2Error::new)?
4127 .ok_or_else(|| GatewayClientV2Error::new("Routing Info not available"))?
4128 .send_fee_minimum
4129 .add_to(amount))
4130 }
4131
4132 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
4133 let rhints = invoice.route_hints();
4134 let hop = rhints.first().and_then(|rh| rh.0.last())?;
4135
4136 let lightning_context = self.await_lightning_context().await;
4139 if hop.src_node_id != lightning_context.lightning_public_key {
4140 return None;
4141 }
4142
4143 self.federation_manager
4144 .read()
4145 .await
4146 .get_client_for_index(hop.short_channel_id)
4147 }
4148
4149 async fn relay_lnv1_swap(
4150 &self,
4151 client: &ClientHandleArc,
4152 invoice: &Bolt11Invoice,
4153 allow_fresh_dispatch: bool,
4154 ) -> std::result::Result<Option<FinalReceiveState>, GatewayClientV2Error> {
4155 let swap_params =
4156 SwapParameters {
4157 payment_hash: *invoice.payment_hash(),
4158 amount_msat: Amount::from_msats(invoice.amount_milli_satoshis().ok_or_else(
4159 || GatewayClientV2Error::new("Amountless invoice not supported"),
4160 )?),
4161 };
4162 let lnv1 = client
4164 .get_first_module::<GatewayClientModule>()
4165 .map_err(|_| GatewayClientV2Error::new("Federation does not have an LNv1 module"))?;
4166 let Some(operation_id) = lnv1
4167 .gateway_handle_direct_swap(swap_params, allow_fresh_dispatch)
4168 .await
4169 .map_err(GatewayClientV2Error::new)?
4170 else {
4171 return Ok(None);
4172 };
4173 let mut stream = lnv1
4174 .gateway_subscribe_ln_receive(operation_id)
4175 .await
4176 .map_err(GatewayClientV2Error::new)?
4177 .into_stream();
4178 let mut final_state = FinalReceiveState::Failure;
4179 while let Some(update) = stream.next().await {
4180 match update {
4181 GatewayExtReceiveStates::Funding => {}
4182 GatewayExtReceiveStates::FundingFailed { error: _ } => {
4183 final_state = FinalReceiveState::Rejected;
4184 }
4185 GatewayExtReceiveStates::Preimage(preimage) => {
4186 final_state = FinalReceiveState::Success(preimage.0);
4187 }
4188 GatewayExtReceiveStates::RefundError {
4189 error_message: _,
4190 error: _,
4191 } => {
4192 final_state = FinalReceiveState::Failure;
4193 }
4194 GatewayExtReceiveStates::RefundSuccess {
4195 out_points: _,
4196 error: _,
4197 } => {
4198 final_state = FinalReceiveState::Refunded;
4199 }
4200 }
4201 }
4202
4203 Ok(Some(final_state))
4204 }
4205
4206 async fn claim_payment_image(
4207 &self,
4208 payment_image: &PaymentImage,
4209 operation_id: OperationId,
4210 ) -> bool {
4211 self.gateway_db
4215 .autocommit(
4216 |dbtx, _| {
4217 let payment_image = payment_image.clone();
4218 Box::pin(async move {
4219 let claimer = dbtx
4220 .claim_outgoing_payment_image(payment_image, operation_id)
4221 .await;
4222 Ok::<_, std::convert::Infallible>(claimer == operation_id)
4223 })
4224 },
4225 None,
4226 )
4227 .await
4228 .expect("Retries until the transaction commits")
4229 }
4230
4231 async fn is_lightning_connected(&self) -> bool {
4232 self.get_lightning_context().await.is_ok()
4233 }
4234
4235 async fn await_lightning_connected(&self) {
4236 self.await_lightning_context().await;
4237 }
4238}
4239
4240#[async_trait]
4241impl IGatewayClientV1 for Gateway {
4242 async fn claim_payment_image(
4243 &self,
4244 payment_image: &PaymentImage,
4245 operation_id: OperationId,
4246 ) -> bool {
4247 IGatewayClientV2::claim_payment_image(self, payment_image, operation_id).await
4248 }
4249
4250 async fn verify_preimage_authentication(
4251 &self,
4252 payment_hash: sha256::Hash,
4253 preimage_auth: sha256::Hash,
4254 contract: OutgoingContractAccount,
4255 ) -> std::result::Result<(), OutgoingPaymentError> {
4256 let mut dbtx = self.gateway_db.begin_transaction().await;
4257 if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
4258 if !PreimageAuth::new(secret_hash).verifies(preimage_auth) {
4259 return Err(OutgoingPaymentError {
4260 error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
4261 contract_id: contract.contract.contract_id(),
4262 contract: Some(contract),
4263 });
4264 }
4265 } else {
4266 dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
4269 .await;
4270 return dbtx
4271 .commit_tx_result()
4272 .await
4273 .map_err(|_| OutgoingPaymentError {
4274 error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
4275 contract_id: contract.contract.contract_id(),
4276 contract: Some(contract),
4277 });
4278 }
4279
4280 Ok(())
4281 }
4282
4283 async fn verify_pruned_invoice(
4284 &self,
4285 payment_data: PaymentData,
4286 ) -> std::result::Result<(), GatewayClientV1Error> {
4287 if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
4288 let lightning_context = self
4289 .get_lightning_context()
4290 .await
4291 .map_err(GatewayClientV1Error::new)?;
4292
4293 if !lightning_context.lnrpc.supports_private_payments() {
4294 return Err(GatewayClientV1Error::new(
4295 "Private payments are not supported by the lightning node",
4296 ));
4297 }
4298 }
4299
4300 Ok(())
4301 }
4302
4303 async fn begin_fresh_receive(&self, federation_id: FederationId) -> Option<PaymentPolicyGuard> {
4304 self.admit_fresh_receive(federation_id).await
4305 }
4306
4307 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
4308 let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
4309 let lightning_fee = gateway_dbtx
4310 .load_federation_config(federation_id)
4311 .await?
4312 .lightning_fee;
4313
4314 RoutingFees::try_from(lightning_fee)
4318 .inspect_err(|err| {
4319 warn!(
4320 target: LOG_GATEWAY,
4321 %federation_id,
4322 err = %err.fmt_compact(),
4323 "Configured lightning fee cannot be used. Set a smaller fee with `set_fees`."
4324 );
4325 })
4326 .ok()
4327 }
4328
4329 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
4330 self.federation_manager
4331 .read()
4332 .await
4333 .client(federation_id)
4334 .cloned()
4335 }
4336
4337 async fn get_client_for_invoice(
4338 &self,
4339 payment_data: PaymentData,
4340 ) -> Option<Spanned<ClientHandleArc>> {
4341 let rhints = payment_data.route_hints();
4342 let hop = rhints.first().and_then(|rh| rh.0.last())?;
4343
4344 let lightning_context = self.await_lightning_context().await;
4347 if hop.src_node_id != lightning_context.lightning_public_key {
4348 return None;
4349 }
4350
4351 self.federation_manager
4352 .read()
4353 .await
4354 .get_client_for_index(hop.short_channel_id)
4355 }
4356
4357 async fn pay(
4358 &self,
4359 payment_data: PaymentData,
4360 max_delay: u64,
4361 max_fee: Amount,
4362 ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
4363 let lightning_context = self.await_lightning_context().await;
4366
4367 match payment_data {
4368 PaymentData::Invoice(invoice) => {
4369 lightning_context
4370 .lnrpc
4371 .pay(invoice, max_delay, max_fee)
4372 .await
4373 }
4374 PaymentData::PrunedInvoice(invoice) => {
4375 lightning_context
4376 .lnrpc
4377 .pay_private(invoice, max_delay, max_fee)
4378 .await
4379 }
4380 }
4381 }
4382
4383 async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
4384 self.await_outbound_payment_exists(payment_hash).await
4385 }
4386
4387 async fn complete_htlc(
4388 &self,
4389 htlc: InterceptPaymentResponse,
4390 ) -> std::result::Result<(), LightningRpcError> {
4391 self.await_complete_htlc(htlc).await
4392 }
4393
4394 async fn is_lnv2_direct_swap(
4395 &self,
4396 payment_hash: sha256::Hash,
4397 amount: Amount,
4398 ) -> std::result::Result<
4399 Option<(
4400 fedimint_lnv2_common::contracts::IncomingContract,
4401 ClientHandleArc,
4402 )>,
4403 GatewayClientV1Error,
4404 > {
4405 let (contract, client) = self
4406 .get_registered_incoming_contract_and_client_v2(
4407 PaymentImage::Hash(payment_hash),
4408 amount.msats,
4409 )
4410 .await
4411 .map_err(GatewayClientV1Error::new)?;
4412 Ok(Some((contract, client)))
4413 }
4414}