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, PaymentStats, PaymentSummaryPayload,
86 PaymentSummaryResponse, PeginFromOnchainPayload, ReceiveEcashPayload, ReceiveEcashResponse,
87 RegisteredProtocol, SendOnchainRequest, SetChannelFeesRequest, SetFeesPayload,
88 SetMnemonicPayload, SetPaymentPolicyPayload, SpendEcashPayload, SpendEcashResponse,
89 V1_API_ENDPOINT, WithdrawPayload, WithdrawPreviewPayload, WithdrawPreviewResponse,
90 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::lnd::GatewayLndClient;
106use fedimint_lightning::{
107 CreateInvoiceRequest, ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse,
108 InvoiceDescription, LightningContext, LightningRpcError, LnRpcTracked, Lnv2HoldInvoiceFilter,
109 PayInvoiceResponse, PaymentAction, RouteHtlcStream, ldk,
110};
111use fedimint_ln_client::pay::PaymentData;
112use fedimint_ln_common::config::LightningClientConfig;
113use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount;
114use fedimint_ln_common::contracts::{IdentifiableContract, Preimage};
115use fedimint_ln_common::{LightningCommonInit, PreimageAuth};
116use fedimint_lnurl::VerifyResponse;
117use fedimint_lnv2_common::Bolt11InvoiceDescription;
118use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage};
119use fedimint_lnv2_common::gateway_api::{
120 CreateBolt11InvoicePayload, MAX_INVOICE_EXPIRY_SECS, PaymentFee, RoutingInfo,
121 SendPaymentPayload,
122};
123use fedimint_logging::LOG_GATEWAY;
124use fedimint_mint_client::{MintClientInit, MintClientModule, OOBNotes, ReissueExternalNotesState};
125use fedimint_mintv2_client::{
126 MintClientInit as MintV2ClientInit, MintClientModule as MintV2ClientModule,
127};
128use fedimint_wallet_client::{PegOutFees, WalletClientInit, WalletClientModule, WithdrawState};
129use futures::stream::StreamExt;
130use lightning_invoice::{Bolt11Invoice, RoutingFees};
131use rand::rngs::OsRng;
132use tokio::sync::RwLock;
133use tracing::{debug, info, info_span, warn};
134
135use crate::envs::FM_GATEWAY_MNEMONIC_ENV;
136use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError};
137use crate::events::get_events_for_duration;
138use crate::rate_limit::TokenBucketRateLimiter;
139use crate::registration_health::RegistrationHealthTracker;
140use crate::rpc_server::run_webserver;
141use crate::types::PrettyInterceptPaymentRequest;
142
143const GW_ANNOUNCEMENT_TTL: Duration = Duration::from_mins(10);
145
146const DEFAULT_NUM_ROUTE_HINTS: u32 = 1;
149
150const DEFAULT_INVOICE_RATE_LIMIT_BURST: u32 = 50;
152
153const DEFAULT_INVOICE_RATE_LIMIT_PER_SECOND: u32 = 5;
156
157pub const DEFAULT_NETWORK: Network = Network::Regtest;
159
160const LIGHTNING_CONTEXT_RETRY_INTERVAL: Duration = Duration::from_secs(5);
163
164const VERIFY_WAIT_TIMEOUT: Duration = Duration::from_secs(30);
172
173pub type Result<T> = std::result::Result<T, PublicGatewayError>;
174pub type AdminResult<T> = std::result::Result<T, AdminGatewayError>;
175
176const DB_FILE: &str = "gatewayd.db";
179
180const LDK_NODE_DB_FOLDER: &str = "ldk_node";
183
184#[cfg_attr(doc, aquamarine::aquamarine)]
185#[derive(Clone, Debug)]
198pub enum GatewayState {
199 NotConfigured {
200 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
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 )
322 .await
323 }
324}
325
326enum ReceivePaymentStreamAction {
328 RetryAfterDelay,
329 NoRetry,
330}
331
332#[derive(Clone)]
333pub struct Gateway {
334 federation_manager: Arc<RwLock<FederationManager>>,
336
337 lightning_mode: LightningMode,
339
340 state: Arc<RwLock<GatewayState>>,
342
343 client_builder: GatewayClientBuilder,
346
347 gateway_db: Database,
349
350 listen: SocketAddr,
352
353 metrics_listen: SocketAddr,
355
356 task_group: TaskGroup,
358
359 bcrypt_password_hash: String,
361
362 bcrypt_liquidity_manager_password_hash: Option<String>,
365
366 num_route_hints: u32,
368
369 network: Network,
371
372 chain_source: ChainSource,
374
375 default_routing_fees: PaymentFee,
377
378 default_transaction_fees: PaymentFee,
380
381 iroh_sk: iroh::SecretKey,
383
384 iroh_listen: Option<SocketAddr>,
386
387 iroh_dns: Option<SafeUrl>,
389
390 iroh_relays: Vec<SafeUrl>,
393
394 registrations: BTreeMap<RegisteredProtocol, Registration>,
397
398 registration_health: RegistrationHealthTracker,
400
401 invoice_rate_limiter: Arc<TokenBucketRateLimiter>,
403
404 lnv2_hold_invoice_lock_pool: Arc<lockable::LockPool<sha256::Hash>>,
408}
409
410impl std::fmt::Debug for Gateway {
411 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412 f.debug_struct("Gateway")
413 .field("federation_manager", &self.federation_manager)
414 .field("state", &self.state)
415 .field("client_builder", &self.client_builder)
416 .field("gateway_db", &self.gateway_db)
417 .field("listen", &self.listen)
418 .field("registrations", &self.registrations)
419 .finish_non_exhaustive()
420 }
421}
422
423struct WithdrawDetails {
425 amount: Amount,
426 mint_fees: Option<Amount>,
427 peg_out_fees: PegOutFees,
428 flat_peg_out_fee: bool,
429}
430
431async fn await_withdraw_change(
440 client: &ClientHandleArc,
441 operation_id: OperationId,
442 change: Vec<fedimint_core::OutPoint>,
443) {
444 if let Err(err) = client
445 .await_primary_bitcoin_module_outputs(operation_id, change)
446 .await
447 {
448 warn!(
449 target: LOG_GATEWAY,
450 err = %err.fmt_compact(),
451 ?operation_id,
452 "Failed to await withdrawal change outputs"
453 );
454 }
455}
456
457async fn withdraw_v2(
459 client: &ClientHandleArc,
460 wallet_module: &fedimint_walletv2_client::WalletClientModule,
461 address: &Address,
462 amount: BitcoinAmountOrAll,
463 quoted_fee: Option<bitcoin::Amount>,
464) -> AdminResult<WithdrawResponse> {
465 let current_fee =
466 wallet_module
467 .send_fee()
468 .await
469 .map_err(|e| AdminGatewayError::WithdrawError {
470 failure_reason: e.fmt_compact().to_string(),
471 })?;
472
473 let fee = match quoted_fee {
478 Some(quoted_fee) if quoted_fee < current_fee => {
479 return Err(AdminGatewayError::WithdrawError {
480 failure_reason: format!(
481 "The on-chain fee increased from {} to {} sats since the preview, please preview the withdrawal again",
482 quoted_fee.to_sat(),
483 current_fee.to_sat()
484 ),
485 });
486 }
487 Some(quoted_fee) => quoted_fee,
488 None => current_fee,
489 };
490
491 let withdraw_amount = match amount {
492 BitcoinAmountOrAll::All => {
493 let balance = client.get_balance_for_btc().await.map_err(|err| {
494 AdminGatewayError::Unexpected(anyhow!(
495 "Balance not available: {}",
496 err.fmt_compact()
497 ))
498 })?;
499
500 wallet_module
505 .max_sendable_amount(balance, fee)
506 .await
507 .map_err(|err| AdminGatewayError::WithdrawError {
508 failure_reason: format!(
509 "Insufficient funds. Balance: {balance} Fee: {fee}: {}",
510 err.fmt_compact()
511 ),
512 })?
513 }
514 BitcoinAmountOrAll::Amount(a) => a,
515 };
516
517 let operation_id = wallet_module
518 .send(
519 address.as_unchecked().clone(),
520 withdraw_amount,
521 Some(fee),
522 serde_json::Value::Null,
523 )
524 .await
525 .map_err(|e| AdminGatewayError::WithdrawError {
526 failure_reason: e.fmt_compact().to_string(),
527 })?;
528
529 let result = wallet_module
530 .await_final_send_operation_state(operation_id)
531 .await
532 .map_err(|e| AdminGatewayError::WithdrawError {
533 failure_reason: e.fmt_compact().to_string(),
534 })?;
535
536 let fees = PegOutFees::from_amount(fee);
537
538 match result {
539 fedimint_walletv2_client::FinalSendOperationState::Success(txid) => {
540 if let Some(Ok(fedimint_walletv2_client::WalletOperationMeta::Send(meta))) = client
541 .operation_log()
542 .get_operation(operation_id)
543 .await
544 .map(|operation| operation.try_meta())
545 {
546 await_withdraw_change(
547 client,
548 operation_id,
549 meta.change_outpoint_range.into_iter().collect(),
550 )
551 .await;
552 }
553 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds via walletv2");
554 Ok(WithdrawResponse { txid, fees })
555 }
556 fedimint_walletv2_client::FinalSendOperationState::Aborted => {
557 Err(AdminGatewayError::WithdrawError {
558 failure_reason: "Withdrawal transaction was aborted".to_string(),
559 })
560 }
561 fedimint_walletv2_client::FinalSendOperationState::Failure => {
562 Err(AdminGatewayError::WithdrawError {
563 failure_reason: "Withdrawal failed".to_string(),
564 })
565 }
566 }
567}
568
569async fn calculate_max_withdrawable(
571 client: &ClientHandleArc,
572 address: &Address,
573) -> AdminResult<WithdrawDetails> {
574 let balance = client.get_balance_for_btc().await.map_err(|err| {
575 AdminGatewayError::Unexpected(anyhow!("Balance not available: {}", err.fmt_compact()))
576 })?;
577
578 if let Ok(wallet_module) =
579 client.get_first_module::<fedimint_walletv2_client::WalletClientModule>()
580 {
581 let fee = wallet_module
582 .send_fee()
583 .await
584 .map_err(|e| AdminGatewayError::WithdrawError {
585 failure_reason: e.fmt_compact().to_string(),
586 })?;
587
588 let max_withdrawable = wallet_module
589 .max_sendable_amount(balance, fee)
590 .await
591 .map_err(|err| AdminGatewayError::WithdrawError {
592 failure_reason: err.fmt_compact().to_string(),
593 })?;
594
595 let federation_fees = balance
600 .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
601 .saturating_sub(Amount::from_sats(fee.to_sat()));
602
603 return Ok(WithdrawDetails {
604 amount: Amount::from_sats(max_withdrawable.to_sat()),
605 mint_fees: Some(federation_fees),
606 peg_out_fees: PegOutFees::from_amount(fee),
607 flat_peg_out_fee: true,
608 });
609 }
610
611 let Ok(wallet_module) = client.get_first_module::<WalletClientModule>() else {
612 return Err(AdminGatewayError::Unexpected(anyhow!(
613 "No wallet module found"
614 )));
615 };
616
617 let (max_withdrawable, peg_out_fees) = wallet_module
618 .max_withdrawable_amount(address, balance)
619 .await
620 .map_err(|err| AdminGatewayError::WithdrawError {
621 failure_reason: err.fmt_compact().to_string(),
622 })?;
623
624 let federation_fees = balance
629 .saturating_sub(Amount::from_sats(max_withdrawable.to_sat()))
630 .saturating_sub(Amount::from_sats(peg_out_fees.amount().to_sat()));
631
632 Ok(WithdrawDetails {
633 amount: Amount::from_sats(max_withdrawable.to_sat()),
634 mint_fees: Some(federation_fees),
635 peg_out_fees,
636 flat_peg_out_fee: false,
637 })
638}
639
640impl Gateway {
641 fn get_bitcoind_client(
644 opts: &GatewayOpts,
645 network: bitcoin::Network,
646 gateway_id: &PublicKey,
647 ) -> anyhow::Result<(BitcoindClient, ChainSource)> {
648 let bitcoind_username = opts
649 .bitcoind_username
650 .clone()
651 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_USERNAME is not");
652 let url = opts.bitcoind_url.clone().expect("No bitcoind url set");
653 let password = opts
654 .bitcoind_password
655 .clone()
656 .expect("FM_BITCOIND_URL is set but FM_BITCOIND_PASSWORD is not");
657
658 let chain_source = ChainSource::Bitcoind {
659 username: bitcoind_username.clone(),
660 password: password.clone(),
661 server_url: url.clone(),
662 };
663 let wallet_name = format!("gatewayd-{gateway_id}");
664 let client = BitcoindClient::new(&url, bitcoind_username, password, &wallet_name, network)?;
665 Ok((client, chain_source))
666 }
667
668 pub async fn new_with_default_modules(
671 mnemonic_sender: tokio::sync::broadcast::Sender<()>,
672 ) -> anyhow::Result<Gateway> {
673 let opts = GatewayOpts::parse();
674 let gateway_parameters = opts.to_gateway_parameters()?;
675 let decoders = ModuleDecoderRegistry::default();
676
677 let db_path = opts.data_dir.join(DB_FILE);
678 let gateway_db = match opts.db_backend {
679 DatabaseBackend::RocksDb => {
680 debug!(target: LOG_GATEWAY, "Using RocksDB database backend");
681 Database::new(
682 fedimint_rocksdb::RocksDb::build(db_path).open().await?,
683 decoders,
684 )
685 }
686 DatabaseBackend::CursedRedb => {
687 debug!(target: LOG_GATEWAY, "Using CursedRedb database backend");
688 Database::new(
689 fedimint_cursed_redb::MemAndRedb::new(db_path).await?,
690 decoders,
691 )
692 }
693 };
694
695 apply_migrations(
698 &gateway_db,
699 (),
700 "gatewayd".to_string(),
701 get_gatewayd_database_migrations(),
702 None,
703 None,
704 )
705 .await?;
706
707 let http_id = Self::load_or_create_gateway_keypair(&gateway_db, RegisteredProtocol::Http)
710 .await
711 .public_key();
712 let (dyn_bitcoin_rpc, chain_source) =
713 match (opts.bitcoind_url.as_ref(), opts.esplora_url.as_ref()) {
714 (Some(_), None) => {
715 let (client, chain_source) =
716 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
717 (client.into_dyn(), chain_source)
718 }
719 (None, Some(url)) => {
720 let client = EsploraClient::new(url)
721 .expect("Could not create EsploraClient")
722 .into_dyn();
723 let chain_source = ChainSource::Esplora {
724 server_url: url.clone(),
725 };
726 (client, chain_source)
727 }
728 (Some(_), Some(_)) => {
729 let (client, chain_source) =
731 Self::get_bitcoind_client(&opts, gateway_parameters.network, &http_id)?;
732 (client.into_dyn(), chain_source)
733 }
734 _ => unreachable!("ArgGroup already enforced XOR relation"),
735 };
736
737 let mut registry = ClientModuleInitRegistry::new();
740 registry.attach(MintClientInit);
741 registry.attach(MintV2ClientInit);
742 registry.attach(WalletClientInit::new(dyn_bitcoin_rpc));
743 registry.attach(fedimint_walletv2_client::WalletClientInit);
744
745 let client_builder =
746 GatewayClientBuilder::new(opts.data_dir.clone(), registry, opts.db_backend).await?;
747
748 let gateway_state = if Self::load_mnemonic(&gateway_db).await.is_some() {
749 GatewayState::Disconnected
750 } else {
751 if gateway_parameters.skip_setup {
754 let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) {
755 info!(target: LOG_GATEWAY, "Using provided mnemonic from environment variable");
756 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(
757 |e| {
758 AdminGatewayError::MnemonicError(anyhow!(format!(
759 "Seed phrase provided in environment was invalid {e:?}"
760 )))
761 },
762 )?
763 } else {
764 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
765 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
766 };
767
768 Client::store_encodable_client_secret(&gateway_db, mnemonic.to_entropy())
769 .await
770 .map_err(|err| AdminGatewayError::MnemonicError(err.into()))?;
771 GatewayState::Disconnected
772 } else {
773 GatewayState::NotConfigured { mnemonic_sender }
774 }
775 };
776
777 info!(
778 target: LOG_GATEWAY,
779 version = %fedimint_build_code_version_env!(),
780 "Starting gatewayd",
781 );
782
783 Gateway::new(
784 opts.mode,
785 gateway_parameters,
786 gateway_db,
787 client_builder,
788 gateway_state,
789 chain_source,
790 )
791 .await
792 }
793
794 async fn new(
797 lightning_mode: LightningMode,
798 gateway_parameters: GatewayParameters,
799 gateway_db: Database,
800 client_builder: GatewayClientBuilder,
801 gateway_state: GatewayState,
802 chain_source: ChainSource,
803 ) -> anyhow::Result<Gateway> {
804 let num_route_hints = gateway_parameters.num_route_hints;
805 let network = gateway_parameters.network;
806
807 let task_group = TaskGroup::new();
808 task_group.install_kill_handler();
809
810 let mut registrations = BTreeMap::new();
811 if let Some(http_url) = gateway_parameters.versioned_api {
812 registrations.insert(
813 RegisteredProtocol::Http,
814 Registration::new(&gateway_db, http_url, RegisteredProtocol::Http).await,
815 );
816 }
817
818 let iroh_sk = Self::load_or_create_iroh_key(&gateway_db).await;
819 if gateway_parameters.iroh_listen.is_some() {
820 let endpoint_url = SafeUrl::parse(&format!("iroh://{}", iroh_sk.public()))?;
821 registrations.insert(
822 RegisteredProtocol::Iroh,
823 Registration::new(&gateway_db, endpoint_url, RegisteredProtocol::Iroh).await,
824 );
825 }
826
827 Ok(Self {
828 federation_manager: Arc::new(RwLock::new(FederationManager::new())),
829 lightning_mode,
830 state: Arc::new(RwLock::new(gateway_state)),
831 client_builder,
832 gateway_db: gateway_db.clone(),
833 listen: gateway_parameters.listen,
834 metrics_listen: gateway_parameters.metrics_listen,
835 task_group,
836 bcrypt_password_hash: gateway_parameters.bcrypt_password_hash.to_string(),
837 bcrypt_liquidity_manager_password_hash: gateway_parameters
838 .bcrypt_liquidity_manager_password_hash
839 .map(|h| h.to_string()),
840 num_route_hints,
841 network,
842 chain_source,
843 default_routing_fees: gateway_parameters.default_routing_fees,
844 default_transaction_fees: gateway_parameters.default_transaction_fees,
845 iroh_sk,
846 iroh_dns: gateway_parameters.iroh_dns,
847 iroh_relays: gateway_parameters.iroh_relays,
848 iroh_listen: gateway_parameters.iroh_listen,
849 registrations,
850 registration_health: RegistrationHealthTracker::default(),
851 invoice_rate_limiter: Arc::new(TokenBucketRateLimiter::new(
852 gateway_parameters.invoice_rate_limit_burst,
853 gateway_parameters.invoice_rate_limit_per_second,
854 )),
855 lnv2_hold_invoice_lock_pool: Arc::new(lockable::LockPool::new()),
856 })
857 }
858
859 async fn load_or_create_gateway_keypair(
860 gateway_db: &Database,
861 protocol: RegisteredProtocol,
862 ) -> secp256k1::Keypair {
863 let mut dbtx = gateway_db.begin_transaction().await;
864 let keypair = dbtx.load_or_create_gateway_keypair(protocol).await;
865 dbtx.commit_tx().await;
866 keypair
867 }
868
869 async fn load_or_create_iroh_key(gateway_db: &Database) -> iroh::SecretKey {
872 let mut dbtx = gateway_db.begin_transaction().await;
873 let iroh_sk = dbtx.load_or_create_iroh_key().await;
874 dbtx.commit_tx().await;
875 iroh_sk
876 }
877
878 pub async fn http_gateway_id(&self) -> PublicKey {
879 Self::load_or_create_gateway_keypair(&self.gateway_db, RegisteredProtocol::Http)
880 .await
881 .public_key()
882 }
883
884 async fn get_state(&self) -> GatewayState {
885 self.state.read().await.clone()
886 }
887
888 pub async fn dump_database(
891 dbtx: &mut DatabaseTransaction<'_>,
892 prefix_names: Vec<String>,
893 ) -> BTreeMap<String, Box<dyn erased_serde::Serialize + Send>> {
894 dbtx.dump_database(prefix_names).await
895 }
896
897 pub async fn run(
902 self,
903 runtime: Arc<tokio::runtime::Runtime>,
904 mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
905 ) -> anyhow::Result<TaskShutdownToken> {
906 install_crypto_provider().await;
907 self.register_clients_timer();
908 self.load_clients().await?;
909 self.start_gateway(runtime, mnemonic_receiver.resubscribe());
910 self.spawn_backup_task();
911 self.spawn_prune_registered_contracts_task();
912 fedimint_metrics::spawn_api_server(self.metrics_listen, self.task_group.clone()).await?;
914 let handle = self.task_group.make_handle();
916 run_webserver(Arc::new(self), mnemonic_receiver.resubscribe()).await?;
917 let shutdown_receiver = handle.make_shutdown_rx();
918 Ok(shutdown_receiver)
919 }
920
921 fn spawn_backup_task(&self) {
924 let self_copy = self.clone();
925 self.task_group
926 .spawn_cancellable_silent("backup ecash", async move {
927 const BACKUP_UPDATE_INTERVAL: Duration = Duration::from_hours(1);
928 let mut interval = tokio::time::interval(BACKUP_UPDATE_INTERVAL);
929 interval.tick().await;
930 loop {
931 {
932 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
933 self_copy.backup_all_federations(&mut dbtx).await;
934 dbtx.commit_tx().await;
935 interval.tick().await;
936 }
937 }
938 });
939 }
940
941 fn spawn_prune_registered_contracts_task(&self) {
945 let self_copy = self.clone();
946 self.task_group.spawn_cancellable_silent(
947 "prune registered incoming contracts",
948 async move {
949 const PRUNE_INTERVAL: Duration = Duration::from_hours(1);
950 const RETENTION_AFTER_EXPIRY: Duration = Duration::from_hours(24);
954
955 let mut interval = tokio::time::interval(PRUNE_INTERVAL);
956 loop {
957 interval.tick().await;
958
959 let cutoff_secs = duration_since_epoch()
960 .saturating_sub(RETENTION_AFTER_EXPIRY)
961 .as_secs();
962
963 let mut dbtx = self_copy.gateway_db.begin_transaction().await;
964 let num_pruned = dbtx.prune_registered_incoming_contracts(cutoff_secs).await;
965 match dbtx.commit_tx_result().await {
966 Ok(()) => {
967 if num_pruned > 0 {
968 info!(
969 target: LOG_GATEWAY,
970 num_pruned,
971 "Pruned expired incoming contract records"
972 );
973 }
974 }
975 Err(err) => {
976 warn!(
977 target: LOG_GATEWAY,
978 err = %err.fmt_compact(),
979 "Failed to prune expired incoming contract records"
980 );
981 }
982 }
983 }
984 },
985 );
986 }
987
988 pub async fn backup_all_federations(&self, dbtx: &mut DatabaseTransaction<'_, Committable>) {
992 const BACKUP_THRESHOLD_DURATION: Duration = Duration::from_hours(24);
995
996 let now = fedimint_core::time::now();
997 let threshold = now
998 .checked_sub(BACKUP_THRESHOLD_DURATION)
999 .expect("Cannot be negative");
1000 for (id, last_backup) in dbtx.load_backup_records().await {
1001 match last_backup {
1002 Some(backup_time) if backup_time < threshold => {
1003 let fed_manager = self.federation_manager.read().await;
1004 fed_manager.backup_federation(&id, dbtx, now).await;
1005 }
1006 None => {
1007 let fed_manager = self.federation_manager.read().await;
1008 fed_manager.backup_federation(&id, dbtx, now).await;
1009 }
1010 _ => {}
1011 }
1012 }
1013 }
1014
1015 fn start_gateway(
1018 &self,
1019 runtime: Arc<tokio::runtime::Runtime>,
1020 mut mnemonic_receiver: tokio::sync::broadcast::Receiver<()>,
1021 ) {
1022 const PAYMENT_STREAM_RETRY_SECONDS: u64 = 60;
1023
1024 let self_copy = self.clone();
1025 let tg = self.task_group.clone();
1026 self.task_group.spawn(
1027 "Subscribe to intercepted lightning payments in stream",
1028 |handle| async move {
1029 loop {
1031 if handle.is_shutting_down() {
1032 info!(target: LOG_GATEWAY, "Gateway lightning payment stream handler loop is shutting down");
1033 break;
1034 }
1035
1036 if let GatewayState::NotConfigured{ .. } = self_copy.get_state().await {
1037 info!(
1038 target: LOG_GATEWAY,
1039 "Waiting for the mnemonic to be set before starting lightning receive loop."
1040 );
1041 info!(
1042 target: LOG_GATEWAY,
1043 "You might need to provide it from the UI or refer to documentation w.r.t how to initialize it."
1044 );
1045
1046 let _ = mnemonic_receiver.recv().await;
1047 info!(
1048 target: LOG_GATEWAY,
1049 "Received mnemonic, attempting to start lightning receive loop"
1050 );
1051 }
1052
1053 let payment_stream_task_group = tg.make_subgroup();
1054 let lnrpc_route = self_copy.create_lightning_client(runtime.clone()).await;
1055
1056 debug!(target: LOG_GATEWAY, "Establishing lightning payment stream...");
1057 let (stream, ln_client) = match lnrpc_route.route_htlcs(&payment_stream_task_group).await
1058 {
1059 Ok((stream, ln_client)) => (stream, ln_client),
1060 Err(err) => {
1061 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to open lightning payment stream");
1062 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
1070 crit!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Lightning payment stream task group shutdown");
1071 }
1072 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
1073 continue
1074 }
1075 };
1076
1077 self_copy.set_gateway_state(GatewayState::Connected).await;
1079 info!(target: LOG_GATEWAY, "Established lightning payment stream");
1080
1081 let route_payments_response =
1082 self_copy.route_lightning_payments(&handle, stream, ln_client).await;
1083
1084 self_copy.set_gateway_state(GatewayState::Disconnected).await;
1085 if let Err(err) = payment_stream_task_group.shutdown_join_all(None).await {
1086 crit!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Lightning payment stream task group shutdown");
1087 }
1088
1089 self_copy.unannounce_from_all_federations().await;
1090
1091 match route_payments_response {
1092 ReceivePaymentStreamAction::RetryAfterDelay => {
1093 warn!(target: LOG_GATEWAY, retry_interval = %PAYMENT_STREAM_RETRY_SECONDS, "Disconnected from lightning node");
1094 sleep(Duration::from_secs(PAYMENT_STREAM_RETRY_SECONDS)).await;
1095 }
1096 ReceivePaymentStreamAction::NoRetry => break,
1097 }
1098 }
1099 },
1100 );
1101 }
1102
1103 async fn route_lightning_payments<'a>(
1107 &'a self,
1108 handle: &TaskHandle,
1109 mut stream: RouteHtlcStream<'a>,
1110 ln_client: Arc<dyn ILnRpcClient>,
1111 ) -> ReceivePaymentStreamAction {
1112 let LightningInfo::Connected {
1113 public_key: lightning_public_key,
1114 alias: lightning_alias,
1115 network: lightning_network,
1116 block_height: _,
1117 synced_to_chain,
1118 } = ln_client.parsed_node_info().await
1119 else {
1120 warn!(target: LOG_GATEWAY, "Failed to retrieve Lightning info");
1121 return ReceivePaymentStreamAction::RetryAfterDelay;
1122 };
1123
1124 assert!(
1125 self.network == lightning_network,
1126 "Lightning node network does not match Gateway's network. LN: {lightning_network} Gateway: {}",
1127 self.network
1128 );
1129
1130 if synced_to_chain || is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
1131 info!(target: LOG_GATEWAY, "Gateway is already synced to chain");
1132 } else {
1133 self.set_gateway_state(GatewayState::Syncing).await;
1134 info!(target: LOG_GATEWAY, "Waiting for chain sync");
1135 if let Err(err) = ln_client.wait_for_chain_sync().await {
1136 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Failed to wait for chain sync");
1137 return ReceivePaymentStreamAction::RetryAfterDelay;
1138 }
1139 }
1140
1141 let lightning_context = LightningContext {
1142 lnrpc: LnRpcTracked::new(ln_client, "gateway"),
1143 lightning_public_key,
1144 lightning_alias,
1145 lightning_network,
1146 };
1147 if let GatewayState::ShuttingDown { .. } = self
1148 .set_gateway_state(GatewayState::Running { lightning_context })
1149 .await
1150 {
1151 info!(
1152 target: LOG_GATEWAY,
1153 "Reconnected to the lightning node while shutting down, not accepting payments"
1154 );
1155 } else {
1156 info!(target: LOG_GATEWAY, "Gateway is running");
1157 }
1158
1159 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
1160 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
1163 let all_federations_configs =
1164 dbtx.load_federation_configs().await.into_iter().collect();
1165 self.register_federations(&all_federations_configs, &self.task_group)
1166 .await;
1167 }
1168
1169 let htlc_task_group = self.task_group.make_subgroup();
1172 if handle
1173 .cancel_on_shutdown(async move {
1174 loop {
1175 let payment_request_or = tokio::select! {
1176 payment_request_or = stream.next() => {
1177 payment_request_or
1178 }
1179 () = self.is_shutting_down_safely() => {
1180 break;
1181 }
1182 };
1183
1184 let Some(payment_request) = payment_request_or else {
1185 warn!(
1186 target: LOG_GATEWAY,
1187 "Unexpected response from incoming lightning payment stream. Shutting down payment processor"
1188 );
1189 break;
1190 };
1191
1192 let state_guard = self.state.read().await;
1193 if let GatewayState::Running { ref lightning_context } = *state_guard {
1194 let gateway = self.clone();
1196 let lightning_context = lightning_context.clone();
1197 htlc_task_group.spawn_cancellable_silent(
1198 "handle_lightning_payment",
1199 async move {
1200 let start = fedimint_core::time::now();
1201 let outcome = gateway
1202 .handle_lightning_payment(payment_request, &lightning_context)
1203 .await;
1204 metrics::HTLC_HANDLING_DURATION_SECONDS
1205 .with_label_values(&[outcome])
1206 .observe(
1207 fedimint_core::time::now()
1208 .duration_since(start)
1209 .unwrap_or_default()
1210 .as_secs_f64(),
1211 );
1212 },
1213 );
1214 } else {
1215 warn!(
1216 target: LOG_GATEWAY,
1217 state = %state_guard,
1218 "Gateway isn't in a running state, cannot handle incoming payments."
1219 );
1220 break;
1221 }
1222 }
1223 })
1224 .await
1225 .is_ok()
1226 {
1227 warn!(target: LOG_GATEWAY, "Lightning payment stream connection broken. Gateway is disconnected");
1228 ReceivePaymentStreamAction::RetryAfterDelay
1229 } else {
1230 info!(target: LOG_GATEWAY, "Received shutdown signal");
1231 ReceivePaymentStreamAction::NoRetry
1232 }
1233 }
1234
1235 async fn is_shutting_down_safely(&self) {
1238 loop {
1239 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
1240 return;
1241 }
1242
1243 fedimint_core::task::sleep(Duration::from_secs(1)).await;
1244 }
1245 }
1246
1247 pub async fn handle_lightning_payment(
1258 &self,
1259 payment_request: InterceptPaymentRequest,
1260 lightning_context: &LightningContext,
1261 ) -> &'static str {
1262 info!(
1263 target: LOG_GATEWAY,
1264 lightning_payment = %PrettyInterceptPaymentRequest(&payment_request),
1265 "Intercepting lightning payment",
1266 );
1267
1268 let _hold_invoice_guard = if payment_request.incoming_circuit().is_none() {
1279 let Some(guard) = self
1280 .lnv2_hold_invoice_lock_pool
1281 .try_lock(payment_request.payment_hash)
1282 else {
1283 info!(
1284 target: LOG_GATEWAY,
1285 payment_hash = %payment_request.payment_hash,
1286 "Dropping duplicate delivery of a HOLD invoice that is already being handled",
1287 );
1288 return "duplicate";
1289 };
1290 Some(guard)
1291 } else {
1292 None
1293 };
1294
1295 let lnv2_start = fedimint_core::time::now();
1296 let lnv2_result = self
1297 .try_handle_lightning_payment_lnv2(&payment_request, lightning_context)
1298 .await;
1299 let lnv2_outcome = if lnv2_result.is_ok() {
1300 "success"
1301 } else {
1302 "error"
1303 };
1304 metrics::HTLC_LNV2_ATTEMPT_DURATION_SECONDS
1305 .with_label_values(&[lnv2_outcome])
1306 .observe(
1307 fedimint_core::time::now()
1308 .duration_since(lnv2_start)
1309 .unwrap_or_default()
1310 .as_secs_f64(),
1311 );
1312 if lnv2_result.is_ok() {
1313 return "lnv2";
1314 }
1315
1316 if payment_request.incoming_circuit().is_none()
1321 && let Err(PublicGatewayError::LNv2(err @ LNv2Error::IncomingAmountMismatch { .. })) =
1322 &lnv2_result
1323 {
1324 warn!(
1325 target: LOG_GATEWAY,
1326 payment_hash = %payment_request.payment_hash,
1327 err = %err.fmt_compact(),
1328 "Ignoring payment for a registered LNv2 contract whose amount does not match",
1329 );
1330 return "ignore";
1331 }
1332
1333 let lnv1_start = fedimint_core::time::now();
1334 let lnv1_result = self
1335 .try_handle_lightning_payment_ln_legacy(&payment_request, lightning_context)
1336 .await;
1337 let lnv1_outcome = if lnv1_result.is_ok() {
1338 "success"
1339 } else {
1340 "error"
1341 };
1342 metrics::HTLC_LNV1_ATTEMPT_DURATION_SECONDS
1343 .with_label_values(&[lnv1_outcome])
1344 .observe(
1345 fedimint_core::time::now()
1346 .duration_since(lnv1_start)
1347 .unwrap_or_default()
1348 .as_secs_f64(),
1349 );
1350 if lnv1_result.is_ok() {
1351 return "lnv1";
1352 }
1353
1354 let is_federation_scid = match payment_request.short_channel_id {
1359 Some(scid) => self
1360 .federation_manager
1361 .read()
1362 .await
1363 .get_client_for_index(scid)
1364 .is_some(),
1365 None => false,
1366 };
1367
1368 let receive_disabled = [&lnv2_result, &lnv1_result]
1374 .into_iter()
1375 .any(|result| matches!(result, Err(PublicGatewayError::ReceiveDisabled { .. })));
1376
1377 if is_federation_scid || receive_disabled {
1378 warn!(
1385 target: LOG_GATEWAY,
1386 payment_hash = %payment_request.payment_hash,
1387 short_channel_id = ?payment_request.short_channel_id,
1388 amount_msat = payment_request.amount_msat,
1389 incoming_chan_id = payment_request.incoming_chan_id,
1390 htlc_id = payment_request.htlc_id,
1391 receive_disabled,
1392 lnv2_err = ?lnv2_result.as_ref().err(),
1393 lnv1_err = ?lnv1_result.as_ref().err(),
1394 "Lightning payment for a served federation could not be accepted: cancelling HTLC",
1395 );
1396 Self::cancel_unmatched_lightning_payment(payment_request, lightning_context).await;
1397 "cancel"
1398 } else {
1399 Self::forward_lightning_payment(payment_request, lightning_context).await;
1404 "forward"
1405 }
1406 }
1407
1408 async fn try_handle_lightning_payment_lnv2(
1411 &self,
1412 htlc_request: &InterceptPaymentRequest,
1413 lightning_context: &LightningContext,
1414 ) -> Result<()> {
1415 let (contract, client) = self
1425 .get_registered_incoming_contract_and_client_v2(
1426 PaymentImage::Hash(htlc_request.payment_hash),
1427 htlc_request.incoming_amount_msat,
1428 )
1429 .await?;
1430
1431 let current_block_height = lightning_context
1434 .lnrpc
1435 .info()
1436 .await
1437 .ok()
1438 .map(|info| info.block_height);
1439 let blocks_to_claim_deadline =
1440 htlc_request.lnv2_blocks_to_claim_deadline(current_block_height);
1441
1442 if let Err(err) = client
1443 .get_first_module::<GatewayClientModuleV2>()
1444 .expect("Must have client module")
1445 .relay_incoming_htlc(
1446 htlc_request.payment_hash,
1447 htlc_request.incoming_chan_id,
1448 htlc_request.htlc_id,
1449 contract,
1450 htlc_request.incoming_amount_msat,
1451 blocks_to_claim_deadline,
1452 )
1453 .await
1454 {
1455 warn!(
1456 target: LOG_GATEWAY,
1457 err = %err.fmt_compact(),
1458 "Error relaying incoming lightning payment"
1459 );
1460
1461 let outcome = InterceptPaymentResponse {
1462 action: PaymentAction::Cancel,
1463 payment_hash: htlc_request.payment_hash,
1464 incoming_chan_id: htlc_request.incoming_chan_id,
1465 htlc_id: htlc_request.htlc_id,
1466 };
1467
1468 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1469 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending HTLC response to lightning node");
1470 }
1471 }
1472
1473 Ok(())
1474 }
1475
1476 async fn try_handle_lightning_payment_ln_legacy(
1479 &self,
1480 htlc_request: &InterceptPaymentRequest,
1481 lightning_context: &LightningContext,
1482 ) -> Result<()> {
1483 let Some(federation_index) = htlc_request.short_channel_id else {
1485 return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1486 "Incoming payment has not last hop short channel id".to_string(),
1487 )));
1488 };
1489
1490 let Some(client) = self
1491 .federation_manager
1492 .read()
1493 .await
1494 .get_client_for_index(federation_index)
1495 else {
1496 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())));
1497 };
1498
1499 let federation_id = client.borrow().with_sync(|client| client.federation_id());
1503 if !self.receive_enabled(federation_id).await {
1504 return Err(PublicGatewayError::ReceiveDisabled {
1505 federation_id_prefix: federation_id.to_prefix(),
1506 });
1507 }
1508
1509 client
1514 .borrow()
1515 .with(|client| async {
1516 let htlc = Htlc::from(htlc_request.clone());
1517 let lnv1 = client
1518 .get_first_module::<GatewayClientModule>()
1519 .map_err(|_| {
1520 PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1521 "Federation does not have LNv1 module".to_string(),
1522 ))
1523 })?;
1524 match lnv1
1525 .gateway_handle_intercepted_htlc(htlc, async {
1526 Ok(lightning_context.lnrpc.info().await?.block_height)
1527 })
1528 .await
1529 {
1530 Ok(_) => Ok(()),
1531 Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment(
1532 format!("Error intercepting lightning payment {}", e.fmt_compact()),
1533 ))),
1534 }
1535 })
1536 .await
1537 }
1538
1539 async fn cancel_unmatched_lightning_payment(
1553 htlc_request: InterceptPaymentRequest,
1554 lightning_context: &LightningContext,
1555 ) {
1556 let outcome = InterceptPaymentResponse {
1557 action: PaymentAction::Cancel,
1558 payment_hash: htlc_request.payment_hash,
1559 incoming_chan_id: htlc_request.incoming_chan_id,
1560 htlc_id: htlc_request.htlc_id,
1561 };
1562
1563 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1564 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1565 }
1566 }
1567
1568 async fn forward_lightning_payment(
1573 htlc_request: InterceptPaymentRequest,
1574 lightning_context: &LightningContext,
1575 ) {
1576 let outcome = InterceptPaymentResponse {
1577 action: PaymentAction::Forward,
1578 payment_hash: htlc_request.payment_hash,
1579 incoming_chan_id: htlc_request.incoming_chan_id,
1580 htlc_id: htlc_request.htlc_id,
1581 };
1582
1583 if let Err(err) = lightning_context.lnrpc.complete_htlc(outcome).await {
1584 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error sending lightning payment response to lightning node");
1585 }
1586 }
1587
1588 async fn set_gateway_state(&self, state: GatewayState) -> GatewayState {
1599 let mut lock = self.state.write().await;
1600
1601 if let GatewayState::ShuttingDown { .. } = *lock {
1602 match state {
1603 GatewayState::Running { lightning_context } => {
1604 *lock = GatewayState::ShuttingDown { lightning_context };
1605 }
1606 ignored => {
1607 info!(
1608 target: LOG_GATEWAY,
1609 ignored_state = %ignored,
1610 "Gateway is shutting down, ignoring state change"
1611 );
1612 }
1613 }
1614 } else {
1615 *lock = state;
1616 }
1617
1618 lock.clone()
1619 }
1620
1621 #[doc(hidden)]
1629 pub async fn set_gateway_state_out_of_band(&self, state: GatewayState) {
1630 self.set_gateway_state(state).await;
1631 }
1632
1633 pub async fn handle_get_federation_config(
1636 &self,
1637 federation_id_or: Option<FederationId>,
1638 ) -> AdminResult<GatewayFedConfig> {
1639 if !matches!(self.get_state().await, GatewayState::Running { .. }) {
1640 return Ok(GatewayFedConfig {
1641 federations: BTreeMap::new(),
1642 });
1643 }
1644
1645 let federations = if let Some(federation_id) = federation_id_or {
1646 let mut federations = BTreeMap::new();
1647 federations.insert(
1648 federation_id,
1649 self.federation_manager
1650 .read()
1651 .await
1652 .get_federation_config(federation_id)
1653 .await?,
1654 );
1655 federations
1656 } else {
1657 self.federation_manager
1658 .read()
1659 .await
1660 .get_all_federation_configs()
1661 .await
1662 };
1663
1664 Ok(GatewayFedConfig { federations })
1665 }
1666
1667 pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult<Address> {
1670 let client = self.select_client(payload.federation_id).await?;
1671
1672 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1673 let address = wallet_module
1674 .allocate_deposit_address_expert_only(())
1675 .await
1676 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?
1677 .address;
1678 Ok(address)
1679 } else if let Ok(wallet_module) = client
1680 .value()
1681 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1682 {
1683 Ok(wallet_module.receive().await)
1684 } else {
1685 Err(AdminGatewayError::Unexpected(anyhow!(
1686 "No wallet module found"
1687 )))
1688 }
1689 }
1690
1691 async fn handle_pay_invoice_msg(
1694 &self,
1695 payload: fedimint_ln_client::pay::PayInvoicePayload,
1696 ) -> Result<Preimage> {
1697 let GatewayState::Running { .. } = self.get_state().await else {
1698 return Err(PublicGatewayError::Lightning(
1699 LightningRpcError::FailedToConnect,
1700 ));
1701 };
1702
1703 debug!(target: LOG_GATEWAY, "Handling pay invoice message");
1704 let client = self.select_client(payload.federation_id).await?;
1705 let contract_id = payload.contract_id;
1706 let gateway_module = &client
1707 .value()
1708 .get_first_module::<GatewayClientModule>()
1709 .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1710 .map_err(PublicGatewayError::LNv1)?;
1711 let operation_id = gateway_module
1712 .gateway_pay_bolt11_invoice(payload)
1713 .await
1714 .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1715 .map_err(PublicGatewayError::LNv1)?;
1716 let mut updates = gateway_module
1717 .gateway_subscribe_ln_pay(operation_id)
1718 .await
1719 .map_err(|err| LNv1Error::OutgoingPayment(err.into()))
1720 .map_err(PublicGatewayError::LNv1)?
1721 .into_stream();
1722 while let Some(update) = updates.next().await {
1723 match update {
1724 GatewayExtPayStates::Success { preimage, .. } => {
1725 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Successfully paid invoice");
1726 return Ok(preimage);
1727 }
1728 GatewayExtPayStates::Fail {
1729 error,
1730 error_message,
1731 } => {
1732 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1733 error: Box::new(error),
1734 message: format!(
1735 "{error_message} while paying invoice with contract id {contract_id}"
1736 ),
1737 }));
1738 }
1739 GatewayExtPayStates::Canceled { error } => {
1740 return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract {
1741 error: Box::new(error.clone()),
1742 message: format!(
1743 "Cancelled with {error} while paying invoice with contract id {contract_id}"
1744 ),
1745 }));
1746 }
1747 GatewayExtPayStates::Created => {
1748 debug!(target: LOG_GATEWAY, contract_id = %contract_id, "Start pay invoice state machine");
1749 }
1750 other => {
1751 debug!(target: LOG_GATEWAY, state = ?other, contract_id = %contract_id, "Got state while paying invoice");
1752 }
1753 }
1754 }
1755
1756 Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment(
1757 anyhow!("Ran out of state updates while paying invoice"),
1758 )))
1759 }
1760
1761 pub async fn handle_backup_msg(
1764 &self,
1765 BackupPayload { federation_id }: BackupPayload,
1766 ) -> AdminResult<()> {
1767 let federation_manager = self.federation_manager.read().await;
1768 let client = federation_manager
1769 .client(&federation_id)
1770 .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!(
1771 format!("Gateway has not connected to {federation_id}")
1772 )))?
1773 .value();
1774 let metadata: BTreeMap<String, String> = BTreeMap::new();
1775 #[allow(deprecated)]
1776 client
1777 .backup_to_federation(fedimint_client::backup::Metadata::from_json_serialized(
1778 metadata,
1779 ))
1780 .await
1781 .map_err(anyhow::Error::from)?;
1782 Ok(())
1783 }
1784
1785 pub async fn handle_recheck_address_msg(
1787 &self,
1788 payload: DepositAddressRecheckPayload,
1789 ) -> AdminResult<()> {
1790 let client = self.select_client(payload.federation_id).await?;
1791
1792 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
1793 wallet_module
1794 .recheck_pegin_address_by_address(payload.address)
1795 .await
1796 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
1797 Ok(())
1798 } else if client
1799 .value()
1800 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
1801 .is_ok()
1802 {
1803 Ok(())
1805 } else {
1806 Err(AdminGatewayError::Unexpected(anyhow!(
1807 "No wallet module found"
1808 )))
1809 }
1810 }
1811
1812 pub async fn handle_receive_ecash_msg(
1814 &self,
1815 payload: ReceiveEcashPayload,
1816 ) -> Result<ReceiveEcashResponse> {
1817 let federation_id_prefix = base32::decode_prefixed::<fedimint_mintv2_client::ECash>(
1819 FEDIMINT_PREFIX,
1820 &payload.notes,
1821 )
1822 .ok()
1823 .and_then(|e| e.mint())
1824 .map(|id| id.to_prefix())
1825 .or_else(|| {
1826 OOBNotes::from_str(&payload.notes)
1827 .ok()
1828 .map(|n| n.federation_id_prefix())
1829 })
1830 .ok_or_else(|| PublicGatewayError::ReceiveEcashError {
1831 failure_reason: "Invalid ecash format: could not parse as ECash or OOBNotes"
1832 .to_string(),
1833 })?;
1834
1835 let client = self
1836 .federation_manager
1837 .read()
1838 .await
1839 .get_client_for_federation_id_prefix(federation_id_prefix)
1840 .ok_or(FederationNotConnected {
1841 federation_id_prefix,
1842 })?;
1843
1844 if let Ok(mint) = client.value().get_first_module::<MintClientModule>() {
1846 let notes = OOBNotes::from_str(&payload.notes).map_err(|e| {
1847 PublicGatewayError::ReceiveEcashError {
1848 failure_reason: format!(
1849 "Expected OOBNotes for MintV1 federation: {}",
1850 e.fmt_compact()
1851 ),
1852 }
1853 })?;
1854 let amount = notes.total_amount();
1855
1856 let operation_id = mint.reissue_external_notes(notes, ()).await.map_err(|e| {
1857 PublicGatewayError::ReceiveEcashError {
1858 failure_reason: e.fmt_compact().to_string(),
1859 }
1860 })?;
1861
1862 let mut updates = mint
1863 .subscribe_reissue_external_notes(operation_id)
1864 .await
1865 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1866 failure_reason: format!(
1867 "Could not subscribe to reissue operation: {}",
1868 e.fmt_compact()
1869 ),
1870 })?
1871 .into_stream();
1872
1873 let mut reissued = false;
1878 while let Some(update) = updates.next().await {
1879 match update {
1880 ReissueExternalNotesState::Failed(failure_reason) => {
1881 return Err(PublicGatewayError::ReceiveEcashError { failure_reason });
1882 }
1883 ReissueExternalNotesState::Done => reissued = true,
1884 ReissueExternalNotesState::Created | ReissueExternalNotesState::Issuing => {}
1885 }
1886 }
1887
1888 if !reissued {
1889 return Err(PublicGatewayError::ReceiveEcashError {
1890 failure_reason: "Reissue operation ended before the notes were reissued"
1891 .to_string(),
1892 });
1893 }
1894
1895 Ok(ReceiveEcashResponse { amount })
1896 } else if let Ok(mint) = client
1897 .value()
1898 .get_primary_module_for_unit::<MintV2ClientModule>(
1899 fedimint_core::module::AmountUnit::BITCOIN,
1900 )
1901 {
1902 let ecash: fedimint_mintv2_client::ECash =
1903 base32::decode_prefixed(FEDIMINT_PREFIX, &payload.notes).map_err(|e| {
1904 PublicGatewayError::ReceiveEcashError {
1905 failure_reason: format!("Expected ECash for MintV2 federation: {e}"),
1906 }
1907 })?;
1908 let amount = ecash.amount();
1909
1910 let operation_id = mint
1911 .receive(ecash, serde_json::Value::Null)
1912 .await
1913 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1914 failure_reason: e.fmt_compact().to_string(),
1915 })?;
1916
1917 let final_state = mint
1918 .await_final_receive_operation_state(operation_id)
1919 .await
1920 .map_err(|e| PublicGatewayError::ReceiveEcashError {
1921 failure_reason: e.fmt_compact().to_string(),
1922 })?;
1923 match final_state {
1924 fedimint_mintv2_client::FinalReceiveOperationState::Success => {}
1925 fedimint_mintv2_client::FinalReceiveOperationState::Rejected => {
1926 return Err(PublicGatewayError::ReceiveEcashError {
1927 failure_reason: "ECash receive was rejected".to_string(),
1928 });
1929 }
1930 }
1931
1932 Ok(ReceiveEcashResponse { amount })
1933 } else {
1934 Err(PublicGatewayError::ReceiveEcashError {
1935 failure_reason: "No mint module found".to_string(),
1936 })
1937 }
1938 }
1939
1940 pub async fn handle_get_invoice_msg(
1943 &self,
1944 payload: GetInvoiceRequest,
1945 ) -> AdminResult<Option<GetInvoiceResponse>> {
1946 let lightning_context = self.get_lightning_context().await?;
1947 let invoice = lightning_context.lnrpc.get_invoice(payload).await?;
1948 Ok(invoice)
1949 }
1950
1951 pub async fn handle_withdraw_to_onchain_msg(
1954 &self,
1955 payload: WithdrawToOnchainPayload,
1956 ) -> AdminResult<WithdrawResponse> {
1957 let address = self.handle_get_ln_onchain_address_msg().await?;
1958 let withdraw = WithdrawPayload {
1959 address: address.into_unchecked(),
1960 federation_id: payload.federation_id,
1961 amount: payload.amount,
1962 quoted_fees: None,
1963 };
1964 self.handle_withdraw_msg(withdraw).await
1965 }
1966
1967 pub async fn handle_pegin_from_onchain_msg(
1970 &self,
1971 payload: PeginFromOnchainPayload,
1972 ) -> AdminResult<Txid> {
1973 let deposit = DepositAddressPayload {
1974 federation_id: payload.federation_id,
1975 };
1976 let address = self.handle_address_msg(deposit).await?;
1977 let send_onchain = SendOnchainRequest {
1978 address: address.into_unchecked(),
1979 amount: payload.amount,
1980 fee_rate_sats_per_vbyte: payload.fee_rate_sats_per_vbyte,
1981 };
1982 let txid = self.handle_send_onchain_msg(send_onchain).await?;
1983
1984 Ok(txid)
1985 }
1986
1987 async fn register_federations(
1995 &self,
1996 federations: &BTreeMap<FederationId, FederationConfig>,
1997 register_task_group: &TaskGroup,
1998 ) {
1999 if let GatewayState::ShuttingDown { .. } = self.get_state().await {
2000 info!(
2001 target: LOG_GATEWAY,
2002 "Gateway is shutting down, skipping federation registration"
2003 );
2004 return;
2005 }
2006
2007 if let Ok(lightning_context) = self.get_lightning_context().await {
2008 let route_hints = lightning_context
2009 .lnrpc
2010 .parsed_route_hints(self.num_route_hints)
2011 .await;
2012 if route_hints.is_empty() {
2013 warn!(target: LOG_GATEWAY, "Gateway did not retrieve any route hints, may reduce receive success rate.");
2014 }
2015
2016 for (federation_id, federation_config) in federations {
2017 let routing_fees = match RoutingFees::try_from(federation_config.lightning_fee) {
2022 Ok(routing_fees) => routing_fees,
2023 Err(err) => {
2024 warn!(
2025 target: LOG_GATEWAY,
2026 %federation_id,
2027 err = %err.fmt_compact(),
2028 "Skipping registration, the configured lightning fee cannot be announced. Set a smaller fee with `set_fees`."
2029 );
2030 continue;
2031 }
2032 };
2033
2034 let fed_manager = self.federation_manager.read().await;
2035 if let Some(client) = fed_manager.client(federation_id) {
2036 let federation_id = *federation_id;
2037 let client_arc = client.clone().into_value();
2038 let route_hints = route_hints.clone();
2039 let lightning_context = lightning_context.clone();
2040 let registration_health = self.registration_health.clone();
2041 let registrations = self
2042 .registrations
2043 .clone()
2044 .into_iter()
2045 .map(|(protocol, registration)| {
2046 let attempt =
2047 registration_health.begin_lnv1_attempt(federation_id, protocol);
2048 (registration, attempt)
2049 })
2050 .collect::<Vec<_>>();
2051
2052 register_task_group.spawn_cancellable_silent(
2053 "register federation",
2054 async move {
2055 let Ok(gateway_client) =
2056 client_arc.get_first_module::<GatewayClientModule>()
2057 else {
2058 return;
2059 };
2060
2061 for (registration, attempt) in registrations {
2062 let succeeded = gateway_client
2063 .try_register_with_federation(
2064 route_hints.clone(),
2065 GW_ANNOUNCEMENT_TTL,
2066 routing_fees,
2067 lightning_context.clone(),
2068 registration.endpoint_url,
2069 registration.keypair,
2070 )
2071 .await;
2072 registration_health
2073 .complete_attempt(
2074 attempt,
2075 succeeded,
2076 fedimint_core::time::now(),
2077 fedimint_core::runtime::Instant::now(),
2078 )
2079 .await;
2080 }
2081 },
2082 );
2083 }
2084 }
2085 }
2086 }
2087
2088 pub async fn select_client(
2091 &self,
2092 federation_id: FederationId,
2093 ) -> std::result::Result<Spanned<fedimint_client::ClientHandleArc>, FederationNotConnected>
2094 {
2095 self.federation_manager
2096 .read()
2097 .await
2098 .client(&federation_id)
2099 .cloned()
2100 .ok_or(FederationNotConnected {
2101 federation_id_prefix: federation_id.to_prefix(),
2102 })
2103 }
2104
2105 async fn load_mnemonic(gateway_db: &Database) -> Option<Mnemonic> {
2106 let secret = Client::load_decodable_client_secret::<Vec<u8>>(gateway_db)
2107 .await
2108 .ok()?;
2109 Mnemonic::from_entropy(&secret).ok()
2110 }
2111
2112 async fn load_clients(&self) -> AdminResult<()> {
2116 if let GatewayState::NotConfigured { .. } = self.get_state().await {
2117 return Ok(());
2118 }
2119
2120 let mut federation_manager = self.federation_manager.write().await;
2121
2122 let configs = {
2123 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
2124 dbtx.load_federation_configs().await
2125 };
2126
2127 if let Some(max_federation_index) = configs.values().map(|cfg| cfg.federation_index).max() {
2128 federation_manager.set_next_index(max_federation_index + 1);
2129 }
2130
2131 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2132 .await
2133 .expect("mnemonic should be set");
2134
2135 for (federation_id, config) in configs {
2136 let federation_index = config.federation_index;
2137 match Box::pin(Spanned::try_new(
2138 info_span!(target: LOG_GATEWAY, "client", federation_id = %federation_id.clone()),
2139 self.client_builder
2140 .build(config, Arc::new(self.clone()), &mnemonic),
2141 ))
2142 .await
2143 {
2144 Ok(client) => {
2145 federation_manager.add_client(federation_index, client);
2146 }
2147 _ => {
2148 warn!(target: LOG_GATEWAY, federation_id = %federation_id, "Failed to load client");
2149 }
2150 }
2151 }
2152
2153 Ok(())
2154 }
2155
2156 fn register_clients_timer(&self) {
2162 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2164 info!(target: LOG_GATEWAY, "Spawning register task...");
2165 let gateway = self.clone();
2166 let register_task_group = self.task_group.make_subgroup();
2167 self.task_group.spawn_cancellable("register clients", async move {
2168 loop {
2169 let gateway_state = gateway.get_state().await;
2170 if let GatewayState::Running { .. } = &gateway_state {
2171 let mut dbtx = gateway.gateway_db.begin_transaction_nc().await;
2172 let all_federations_configs = dbtx.load_federation_configs().await.into_iter().collect();
2173 gateway.register_federations(&all_federations_configs, ®ister_task_group).await;
2174 } else {
2175 const NOT_RUNNING_RETRY: Duration = Duration::from_secs(10);
2177 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");
2178 sleep(NOT_RUNNING_RETRY).await;
2179 continue;
2180 }
2181
2182 sleep(GW_ANNOUNCEMENT_TTL.mul_f32(0.85)).await;
2185 }
2186 });
2187 }
2188 }
2189
2190 async fn check_federation_network(
2193 client: &ClientHandleArc,
2194 network: Network,
2195 ) -> AdminResult<()> {
2196 let federation_id = client.federation_id();
2197 let config = client.config().await;
2198
2199 let lnv1_cfg = config
2200 .modules
2201 .values()
2202 .find(|m| LightningCommonInit::KIND == m.kind);
2203
2204 let lnv2_cfg = config
2205 .modules
2206 .values()
2207 .find(|m| fedimint_lnv2_common::LightningCommonInit::KIND == m.kind);
2208
2209 if lnv1_cfg.is_none() && lnv2_cfg.is_none() {
2211 return Err(AdminGatewayError::ClientCreationError(anyhow!(
2212 "Federation {federation_id} does not have any lightning module (LNv1 or LNv2)"
2213 )));
2214 }
2215
2216 if let Some(cfg) = lnv1_cfg {
2218 let ln_cfg: &LightningClientConfig = cfg.cast().map_err(anyhow::Error::from)?;
2219
2220 if ln_cfg.network.0 != network {
2221 crit!(
2222 target: LOG_GATEWAY,
2223 federation_id = %federation_id,
2224 network = %network,
2225 "Incorrect LNv1 network for federation",
2226 );
2227 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2228 "Unsupported LNv1 network {}",
2229 ln_cfg.network
2230 ))));
2231 }
2232 }
2233
2234 if let Some(cfg) = lnv2_cfg {
2236 let ln_cfg: &fedimint_lnv2_common::config::LightningClientConfig =
2237 cfg.cast().map_err(anyhow::Error::from)?;
2238
2239 if ln_cfg.network != network {
2240 crit!(
2241 target: LOG_GATEWAY,
2242 federation_id = %federation_id,
2243 network = %network,
2244 "Incorrect LNv2 network for federation",
2245 );
2246 return Err(AdminGatewayError::ClientCreationError(anyhow!(format!(
2247 "Unsupported LNv2 network {}",
2248 ln_cfg.network
2249 ))));
2250 }
2251 }
2252
2253 Ok(())
2254 }
2255
2256 pub async fn get_lightning_context(
2266 &self,
2267 ) -> std::result::Result<LightningContext, LightningRpcError> {
2268 match self.get_state().await {
2269 GatewayState::Running { lightning_context }
2270 | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context),
2271 _ => Err(LightningRpcError::FailedToConnect),
2272 }
2273 }
2274
2275 async fn await_lightning_context(&self) -> LightningContext {
2296 loop {
2297 match self.get_lightning_context().await {
2298 Ok(lightning_context) => return lightning_context,
2299 Err(err) => {
2300 let state = self.get_state().await;
2301
2302 warn!(
2303 target: LOG_GATEWAY,
2304 err = %err.fmt_compact(),
2305 %state,
2306 retry_interval_secs = LIGHTNING_CONTEXT_RETRY_INTERVAL.as_secs(),
2307 "Not connected to the lightning node, waiting before asking it again",
2308 );
2309
2310 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
2311 }
2312 }
2313 }
2314 }
2315
2316 pub async fn unannounce_from_all_federations(&self) {
2319 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2320 for registration in self.registrations.values() {
2321 self.federation_manager
2322 .read()
2323 .await
2324 .unannounce_from_all_federations(registration.keypair)
2325 .await;
2326 }
2327 }
2328 }
2329
2330 async fn create_lightning_client(
2331 &self,
2332 runtime: Arc<tokio::runtime::Runtime>,
2333 ) -> Box<dyn ILnRpcClient> {
2334 match self.lightning_mode.clone() {
2335 LightningMode::Lnd {
2336 lnd_rpc_addr,
2337 lnd_tls_cert,
2338 lnd_macaroon,
2339 lnd_time_pref,
2340 lnd_payment_timeout_secs,
2341 } => {
2342 let gateway_db = self.gateway_db.clone();
2347 let lnv2_filter: Lnv2HoldInvoiceFilter = Arc::new(move |hash| {
2348 let gateway_db = gateway_db.clone();
2349 Box::pin(async move {
2350 gateway_db
2351 .begin_transaction_nc()
2352 .await
2353 .load_registered_incoming_contract(PaymentImage::Hash(hash))
2354 .await
2355 .is_some()
2356 })
2357 });
2358
2359 Box::new(GatewayLndClient::new(
2360 lnd_rpc_addr,
2361 lnd_tls_cert,
2362 lnd_macaroon,
2363 lnd_time_pref,
2364 lnd_payment_timeout_secs,
2365 None,
2366 lnv2_filter,
2367 ))
2368 }
2369 LightningMode::Ldk {
2370 lightning_port,
2371 alias,
2372 } => {
2373 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2374 .await
2375 .expect("mnemonic should be set");
2376 retry("create LDK Node", fibonacci_max_one_hour(), || async {
2380 ldk::GatewayLdkClient::new(
2381 &self.client_builder.data_dir().join(LDK_NODE_DB_FOLDER),
2382 self.chain_source.clone(),
2383 self.network,
2384 lightning_port,
2385 alias.clone(),
2386 mnemonic.clone(),
2387 runtime.clone(),
2388 )
2389 .map(Box::new)
2390 .map_err(anyhow::Error::from)
2393 })
2394 .await
2395 .expect("Could not create LDK Node")
2396 }
2397 }
2398 }
2399}
2400
2401#[async_trait]
2402impl IAdminGateway for Gateway {
2403 type Error = AdminGatewayError;
2404
2405 async fn handle_get_info(&self) -> AdminResult<GatewayInfo> {
2408 let GatewayState::Running { lightning_context } = self.get_state().await else {
2409 return Ok(GatewayInfo {
2410 federations: vec![],
2411 federation_fake_scids: None,
2412 version_hash: fedimint_build_code_version_env!().to_string(),
2413 gateway_state: self.state.read().await.to_string(),
2414 lightning_info: LightningInfo::NotConnected,
2415 lightning_mode: self.lightning_mode.clone(),
2416 registrations: self
2417 .registrations
2418 .iter()
2419 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2420 .collect(),
2421 });
2422 };
2423
2424 let dbtx = self.gateway_db.begin_transaction_nc().await;
2425 let federations = self
2426 .federation_manager
2427 .read()
2428 .await
2429 .federation_info_all_federations(dbtx)
2430 .await;
2431
2432 let channels: BTreeMap<u64, FederationId> = federations
2433 .iter()
2434 .map(|federation_info| {
2435 (
2436 federation_info.config.federation_index,
2437 federation_info.federation_id,
2438 )
2439 })
2440 .collect();
2441
2442 let lightning_info = lightning_context.lnrpc.parsed_node_info().await;
2443
2444 Ok(GatewayInfo {
2445 federations,
2446 federation_fake_scids: Some(channels),
2447 version_hash: fedimint_build_code_version_env!().to_string(),
2448 gateway_state: self.state.read().await.to_string(),
2449 lightning_info,
2450 lightning_mode: self.lightning_mode.clone(),
2451 registrations: self
2452 .registrations
2453 .iter()
2454 .map(|(k, v)| (k.clone(), (v.endpoint_url.clone(), v.keypair.public_key())))
2455 .collect(),
2456 })
2457 }
2458
2459 async fn handle_list_channels_msg(
2462 &self,
2463 ) -> AdminResult<Vec<fedimint_gateway_common::ChannelInfo>> {
2464 let context = self.get_lightning_context().await?;
2465 let response = context.lnrpc.list_channels().await?;
2466 Ok(response.channels)
2467 }
2468
2469 async fn handle_payment_summary_msg(
2472 &self,
2473 PaymentSummaryPayload {
2474 start_millis,
2475 end_millis,
2476 }: PaymentSummaryPayload,
2477 ) -> AdminResult<PaymentSummaryResponse> {
2478 let federation_manager = self.federation_manager.read().await;
2479 let fed_configs = federation_manager.get_all_federation_configs().await;
2480 let federation_ids = fed_configs.keys().collect::<Vec<_>>();
2481 let start = UNIX_EPOCH + Duration::from_millis(start_millis);
2482 let end = UNIX_EPOCH + Duration::from_millis(end_millis);
2483
2484 if start > end {
2485 return Err(AdminGatewayError::Unexpected(anyhow!("Invalid time range")));
2486 }
2487
2488 let mut outgoing = StructuredPaymentEvents::default();
2489 let mut incoming = StructuredPaymentEvents::default();
2490 for fed_id in federation_ids {
2491 let client = federation_manager
2492 .client(fed_id)
2493 .expect("No client available")
2494 .value();
2495 let all_events = &get_events_for_duration(client, start, end).await;
2496
2497 let (mut lnv1_outgoing, mut lnv1_incoming) = compute_lnv1_stats(all_events);
2498 let (mut lnv2_outgoing, mut lnv2_incoming) = compute_lnv2_stats(all_events);
2499 outgoing.combine(&mut lnv1_outgoing);
2500 incoming.combine(&mut lnv1_incoming);
2501 outgoing.combine(&mut lnv2_outgoing);
2502 incoming.combine(&mut lnv2_incoming);
2503 }
2504
2505 Ok(PaymentSummaryResponse {
2506 outgoing: PaymentStats::compute(&outgoing),
2507 incoming: PaymentStats::compute(&incoming),
2508 })
2509 }
2510
2511 async fn handle_leave_federation(
2516 &self,
2517 payload: LeaveFedPayload,
2518 ) -> AdminResult<FederationInfo> {
2519 let mut federation_manager = self.federation_manager.write().await;
2522 let mut dbtx = self.gateway_db.begin_transaction().await;
2523
2524 let federation_info = federation_manager
2525 .leave_federation(
2526 payload.federation_id,
2527 &mut dbtx.to_ref_nc(),
2528 self.registrations.values().collect(),
2529 )
2530 .await?;
2531
2532 dbtx.remove_federation_config(payload.federation_id).await;
2533 dbtx.commit_tx().await;
2534 self.registration_health
2535 .clear_federation(payload.federation_id)
2536 .await;
2537 Ok(federation_info)
2538 }
2539
2540 async fn handle_connect_federation(
2545 &self,
2546 payload: ConnectFedPayload,
2547 ) -> AdminResult<FederationInfo> {
2548 let GatewayState::Running { lightning_context } = self.get_state().await else {
2549 return Err(AdminGatewayError::Lightning(
2550 LightningRpcError::FailedToConnect,
2551 ));
2552 };
2553
2554 let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| {
2555 AdminGatewayError::ClientCreationError(anyhow!(format!(
2556 "Invalid federation member string {e:?}"
2557 )))
2558 })?;
2559
2560 let federation_id = invite_code.federation_id();
2561
2562 let mut federation_manager = self.federation_manager.write().await;
2563
2564 if federation_manager.has_federation(federation_id) {
2566 return Err(AdminGatewayError::ClientCreationError(anyhow!(
2567 "Federation has already been registered"
2568 )));
2569 }
2570
2571 let federation_index = federation_manager.pop_next_index()?;
2574
2575 let federation_config = FederationConfig {
2576 invite_code,
2577 federation_index,
2578 lightning_fee: self.default_routing_fees,
2579 transaction_fee: self.default_transaction_fees,
2580 payment_policies: BTreeSet::new(),
2581 _connector: ConnectorType::Tcp,
2583 };
2584
2585 let routing_fees = RoutingFees::try_from(federation_config.lightning_fee)
2589 .map_err(|err| AdminGatewayError::GatewayConfigurationError(err.to_string()))?;
2590
2591 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2592 .await
2593 .expect("mnemonic should be set");
2594 let recover = payload.recover.unwrap_or(false);
2595 if recover {
2596 self.client_builder
2597 .recover(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2598 .await?;
2599 }
2600
2601 let client = self
2602 .client_builder
2603 .build(federation_config.clone(), Arc::new(self.clone()), &mnemonic)
2604 .await?;
2605
2606 if recover {
2607 client.wait_for_all_active_state_machines().await;
2608 }
2609
2610 let federation_info = FederationInfo {
2613 federation_id,
2614 federation_name: federation_manager.federation_name(&client).await,
2615 balance_msat: client.get_balance_for_btc().await.unwrap_or_else(|err| {
2616 warn!(
2617 target: LOG_GATEWAY,
2618 err = %err.fmt_compact(),
2619 %federation_id,
2620 "Balance not immediately available after joining/recovering."
2621 );
2622 Amount::default()
2623 }),
2624 config: federation_config.clone(),
2625 last_backup_time: None,
2626 };
2627
2628 Self::check_federation_network(&client, self.network).await?;
2629 if matches!(self.lightning_mode, LightningMode::Lnd { .. })
2630 && let Ok(lnv1) = client.get_first_module::<GatewayClientModule>()
2631 {
2632 for (protocol, registration) in &self.registrations {
2633 let attempt = self
2634 .registration_health
2635 .begin_lnv1_attempt(federation_id, protocol.clone());
2636 let succeeded = lnv1
2637 .try_register_with_federation(
2638 Vec::new(),
2640 GW_ANNOUNCEMENT_TTL,
2641 routing_fees,
2642 lightning_context.clone(),
2643 registration.endpoint_url.clone(),
2644 registration.keypair,
2645 )
2646 .await;
2647 self.registration_health
2648 .complete_attempt(
2649 attempt,
2650 succeeded,
2651 fedimint_core::time::now(),
2652 fedimint_core::runtime::Instant::now(),
2653 )
2654 .await;
2655 }
2656 }
2657
2658 federation_manager.add_client(
2660 federation_index,
2661 Spanned::new(
2662 info_span!(target: LOG_GATEWAY, "client", federation_id=%federation_id.clone()),
2663 async { client },
2664 )
2665 .await,
2666 );
2667
2668 let mut dbtx = self.gateway_db.begin_transaction().await;
2669 dbtx.save_federation_config(&federation_config).await;
2670 dbtx.save_federation_backup_record(federation_id, None)
2671 .await;
2672 dbtx.commit_tx().await;
2673 debug!(
2674 target: LOG_GATEWAY,
2675 federation_id = %federation_id,
2676 federation_index = %federation_index,
2677 "Federation connected"
2678 );
2679
2680 Ok(federation_info)
2681 }
2682
2683 async fn handle_set_fees_msg(
2686 &self,
2687 SetFeesPayload {
2688 federation_id,
2689 lightning_base,
2690 lightning_parts_per_million,
2691 transaction_base,
2692 transaction_parts_per_million,
2693 }: SetFeesPayload,
2694 ) -> AdminResult<()> {
2695 let mut dbtx = self.gateway_db.begin_transaction().await;
2696 let mut fed_configs = if let Some(fed_id) = federation_id {
2697 dbtx.load_federation_configs()
2698 .await
2699 .into_iter()
2700 .filter(|(id, _)| *id == fed_id)
2701 .collect::<BTreeMap<_, _>>()
2702 } else {
2703 dbtx.load_federation_configs().await
2704 };
2705
2706 let federation_manager = self.federation_manager.read().await;
2707
2708 for (federation_id, config) in &mut fed_configs {
2709 let mut lightning_fee = config.lightning_fee;
2710 if let Some(lightning_base) = lightning_base {
2711 lightning_fee.base = lightning_base;
2712 }
2713
2714 if let Some(lightning_ppm) = lightning_parts_per_million {
2715 lightning_fee.parts_per_million = lightning_ppm;
2716 }
2717
2718 let mut transaction_fee = config.transaction_fee;
2719 if let Some(transaction_base) = transaction_base {
2720 transaction_fee.base = transaction_base;
2721 }
2722
2723 if let Some(transaction_ppm) = transaction_parts_per_million {
2724 transaction_fee.parts_per_million = transaction_ppm;
2725 }
2726
2727 federation_manager
2730 .client(federation_id)
2731 .ok_or(FederationNotConnected {
2732 federation_id_prefix: federation_id.to_prefix(),
2733 })?;
2734
2735 let send_fees = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
2741 AdminGatewayError::GatewayConfigurationError(format!(
2742 "Total Send fees overflowed, they may not exceed {}",
2743 PaymentFee::SEND_FEE_LIMIT
2744 ))
2745 })?;
2746
2747 if !send_fees.is_within(&PaymentFee::SEND_FEE_LIMIT) {
2749 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2750 "Total Send fees exceeded {}",
2751 PaymentFee::SEND_FEE_LIMIT
2752 )));
2753 }
2754
2755 if !transaction_fee.is_within(&PaymentFee::RECEIVE_FEE_LIMIT) {
2757 return Err(AdminGatewayError::GatewayConfigurationError(format!(
2758 "Transaction fees exceeded RECEIVE LIMIT {}",
2759 PaymentFee::RECEIVE_FEE_LIMIT
2760 )));
2761 }
2762
2763 config.lightning_fee = lightning_fee;
2764 config.transaction_fee = transaction_fee;
2765 dbtx.save_federation_config(config).await;
2766 }
2767
2768 dbtx.commit_tx().await;
2769
2770 if matches!(self.lightning_mode, LightningMode::Lnd { .. }) {
2771 let register_task_group = TaskGroup::new();
2772
2773 self.register_federations(&fed_configs, ®ister_task_group)
2774 .await;
2775 }
2776
2777 Ok(())
2778 }
2779
2780 async fn handle_set_payment_policy_msg(
2785 &self,
2786 SetPaymentPolicyPayload {
2787 federation_id,
2788 receive_enabled,
2789 }: SetPaymentPolicyPayload,
2790 ) -> AdminResult<()> {
2791 let mut dbtx = self.gateway_db.begin_transaction().await;
2792 let mut fed_configs = if let Some(fed_id) = federation_id {
2793 dbtx.load_federation_configs()
2794 .await
2795 .into_iter()
2796 .filter(|(id, _)| *id == fed_id)
2797 .collect::<BTreeMap<_, _>>()
2798 } else {
2799 dbtx.load_federation_configs().await
2800 };
2801
2802 if let Some(fed_id) = federation_id
2805 && fed_configs.is_empty()
2806 {
2807 return Err(FederationNotConnected {
2808 federation_id_prefix: fed_id.to_prefix(),
2809 }
2810 .into());
2811 }
2812
2813 let federation_manager = self.federation_manager.read().await;
2814
2815 for (federation_id, config) in &mut fed_configs {
2816 federation_manager
2817 .client(federation_id)
2818 .ok_or(FederationNotConnected {
2819 federation_id_prefix: federation_id.to_prefix(),
2820 })?;
2821
2822 if let Some(receive_enabled) = receive_enabled {
2823 if receive_enabled {
2824 config
2825 .payment_policies
2826 .remove(&PaymentPolicy::ReceivesDisabled);
2827 } else {
2828 config
2829 .payment_policies
2830 .insert(PaymentPolicy::ReceivesDisabled);
2831 }
2832 }
2833
2834 info!(
2835 target: LOG_GATEWAY,
2836 %federation_id,
2837 payment_policies = ?config.payment_policies,
2838 "Updated federation payment policy"
2839 );
2840
2841 dbtx.save_federation_config(config).await;
2842 }
2843
2844 dbtx.commit_tx().await;
2845
2846 Ok(())
2847 }
2848
2849 async fn handle_mnemonic_msg(&self) -> AdminResult<MnemonicResponse> {
2853 let mnemonic = Self::load_mnemonic(&self.gateway_db)
2854 .await
2855 .expect("mnemonic should be set");
2856 let words = mnemonic
2857 .words()
2858 .map(std::string::ToString::to_string)
2859 .collect::<Vec<_>>();
2860 let all_federations = self
2861 .federation_manager
2862 .read()
2863 .await
2864 .get_all_federation_configs()
2865 .await
2866 .keys()
2867 .copied()
2868 .collect::<BTreeSet<_>>();
2869 let legacy_federations = self.client_builder.legacy_federations(all_federations);
2870 let mnemonic_response = MnemonicResponse {
2871 mnemonic: words,
2872 legacy_federations,
2873 };
2874 Ok(mnemonic_response)
2875 }
2876
2877 async fn handle_open_channel_msg(&self, payload: OpenChannelRequest) -> AdminResult<Txid> {
2880 info!(target: LOG_GATEWAY, pubkey = %payload.pubkey, host = %payload.host, amount = %payload.channel_size_sats, "Opening Lightning channel...");
2881 let context = self.get_lightning_context().await?;
2882 let res = context.lnrpc.open_channel(payload).await?;
2883 info!(target: LOG_GATEWAY, txid = %res.funding_txid, "Initiated channel open");
2884 Txid::from_str(&res.funding_txid).map_err(|e| {
2885 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2886 failure_reason: format!("Received invalid channel funding txid string {e}"),
2887 })
2888 })
2889 }
2890
2891 async fn handle_connect_peer_msg(&self, payload: ConnectPeerRequest) -> AdminResult<()> {
2894 info!(
2895 target: LOG_GATEWAY,
2896 pubkey = %payload.node_address.pubkey,
2897 host = %payload.node_address.host_with_port(),
2898 "Connecting to Lightning peer..."
2899 );
2900 let context = self.get_lightning_context().await?;
2901 context.lnrpc.connect_peer(payload).await?;
2902 info!(target: LOG_GATEWAY, "Connected to Lightning peer");
2903 Ok(())
2904 }
2905
2906 async fn handle_close_channels_with_peer_msg(
2909 &self,
2910 payload: CloseChannelsWithPeerRequest,
2911 ) -> AdminResult<CloseChannelsWithPeerResponse> {
2912 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Closing lightning channel...");
2913 let context = self.get_lightning_context().await?;
2914 let response = context
2915 .lnrpc
2916 .close_channels_with_peer(payload.clone())
2917 .await?;
2918 info!(target: LOG_GATEWAY, close_channel_request = %payload, "Initiated channel closure");
2919 Ok(response)
2920 }
2921
2922 async fn handle_set_channel_fees_msg(&self, payload: SetChannelFeesRequest) -> AdminResult<()> {
2925 info!(
2926 target: LOG_GATEWAY,
2927 funding_outpoint = %payload.funding_outpoint,
2928 base_fee_msat = payload.base_fee_msat,
2929 parts_per_million = payload.parts_per_million,
2930 "Updating channel fees..."
2931 );
2932 let context = self.get_lightning_context().await?;
2933 context.lnrpc.set_channel_fees(payload).await?;
2934 Ok(())
2935 }
2936
2937 async fn handle_get_balances_msg(&self) -> AdminResult<GatewayBalances> {
2940 let dbtx = self.gateway_db.begin_transaction_nc().await;
2941 let federation_infos = self
2942 .federation_manager
2943 .read()
2944 .await
2945 .federation_info_all_federations(dbtx)
2946 .await;
2947
2948 let ecash_balances: Vec<FederationBalanceInfo> = federation_infos
2949 .iter()
2950 .map(|federation_info| FederationBalanceInfo {
2951 federation_id: federation_info.federation_id,
2952 ecash_balance_msats: Amount {
2953 msats: federation_info.balance_msat.msats,
2954 },
2955 })
2956 .collect();
2957
2958 let context = self.get_lightning_context().await?;
2959 let lightning_node_balances = context.lnrpc.get_balances().await?;
2960
2961 Ok(GatewayBalances {
2962 onchain_balance_sats: lightning_node_balances.onchain_balance_sats,
2963 lightning_balance_msats: lightning_node_balances.lightning_balance_msats,
2964 ecash_balances,
2965 inbound_lightning_liquidity_msats: lightning_node_balances
2966 .inbound_lightning_liquidity_msats,
2967 })
2968 }
2969
2970 async fn handle_send_onchain_msg(&self, payload: SendOnchainRequest) -> AdminResult<Txid> {
2972 let context = self.get_lightning_context().await?;
2973 let response = context.lnrpc.send_onchain(payload.clone()).await?;
2974 let txid =
2975 Txid::from_str(&response.txid).map_err(|e| AdminGatewayError::WithdrawError {
2976 failure_reason: format!("Failed to parse withdrawal TXID: {e}"),
2977 })?;
2978 info!(onchain_request = %payload, txid = %txid, "Sent onchain transaction");
2979 Ok(txid)
2980 }
2981
2982 async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult<Address> {
2984 let context = self.get_lightning_context().await?;
2985 let response = context.lnrpc.get_ln_onchain_address().await?;
2986
2987 let address = Address::from_str(&response.address).map_err(|e| {
2988 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2989 failure_reason: e.to_string(),
2990 })
2991 })?;
2992
2993 address.require_network(self.network).map_err(|e| {
2994 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
2995 failure_reason: e.to_string(),
2996 })
2997 })
2998 }
2999
3000 async fn handle_deposit_address_msg(
3001 &self,
3002 payload: DepositAddressPayload,
3003 ) -> AdminResult<Address> {
3004 self.handle_address_msg(payload).await
3005 }
3006
3007 async fn handle_receive_ecash_msg(
3008 &self,
3009 payload: ReceiveEcashPayload,
3010 ) -> AdminResult<ReceiveEcashResponse> {
3011 Self::handle_receive_ecash_msg(self, payload)
3012 .await
3013 .map_err(|e| AdminGatewayError::Unexpected(anyhow::anyhow!("{e}")))
3014 }
3015
3016 async fn handle_create_invoice_for_operator_msg(
3019 &self,
3020 payload: CreateInvoiceForOperatorPayload,
3021 ) -> AdminResult<Bolt11Invoice> {
3022 let GatewayState::Running { lightning_context } = self.get_state().await else {
3023 return Err(AdminGatewayError::Lightning(
3024 LightningRpcError::FailedToConnect,
3025 ));
3026 };
3027
3028 Bolt11Invoice::from_str(
3029 &lightning_context
3030 .lnrpc
3031 .create_invoice(CreateInvoiceRequest {
3032 payment_hash: None, amount_msat: payload.amount_msats,
3035 expiry_secs: payload.expiry_secs.unwrap_or(3600),
3036 description: payload.description.map(InvoiceDescription::Direct),
3037 })
3038 .await?
3039 .invoice,
3040 )
3041 .map_err(|e| {
3042 AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata {
3043 failure_reason: e.to_string(),
3044 })
3045 })
3046 }
3047
3048 async fn handle_pay_invoice_for_operator_msg(
3051 &self,
3052 payload: PayInvoiceForOperatorPayload,
3053 ) -> AdminResult<Preimage> {
3054 const BASE_FEE: u64 = 50;
3056 const FEE_DENOMINATOR: u64 = 100;
3057 const MAX_DELAY: u64 = 1008;
3058
3059 let GatewayState::Running { lightning_context } = self.get_state().await else {
3060 return Err(AdminGatewayError::Lightning(
3061 LightningRpcError::FailedToConnect,
3062 ));
3063 };
3064
3065 let max_fee = BASE_FEE
3066 + payload
3067 .invoice
3068 .amount_milli_satoshis()
3069 .context("Invoice is missing amount")?
3070 .saturating_div(FEE_DENOMINATOR);
3071
3072 let res = lightning_context
3073 .lnrpc
3074 .pay(payload.invoice, MAX_DELAY, Amount::from_msats(max_fee))
3075 .await?;
3076 Ok(res.preimage)
3077 }
3078
3079 async fn handle_list_transactions_msg(
3081 &self,
3082 payload: ListTransactionsPayload,
3083 ) -> AdminResult<ListTransactionsResponse> {
3084 let lightning_context = self.get_lightning_context().await?;
3085 let response = lightning_context
3086 .lnrpc
3087 .list_transactions(payload.start_secs, payload.end_secs)
3088 .await?;
3089 Ok(response)
3090 }
3091
3092 async fn handle_spend_ecash_msg(
3094 &self,
3095 payload: SpendEcashPayload,
3096 ) -> AdminResult<SpendEcashResponse> {
3097 let client = self
3098 .select_client(payload.federation_id)
3099 .await?
3100 .into_value();
3101
3102 if let Ok(mint_module) = client.get_first_module::<MintClientModule>() {
3103 let notes = mint_module
3104 .send_oob_notes(payload.amount, ())
3105 .await
3106 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
3107 debug!(target: LOG_GATEWAY, ?notes, "Spend ecash notes");
3108 Ok(SpendEcashResponse {
3109 notes: notes.to_string(),
3110 })
3111 } else if let Ok(mint_module) = client.get_primary_module_for_unit::<MintV2ClientModule>(
3112 fedimint_core::module::AmountUnit::BITCOIN,
3113 ) {
3114 let (_, ecash) = mint_module
3115 .send(payload.amount, serde_json::Value::Null, true)
3116 .await
3117 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
3118
3119 Ok(SpendEcashResponse {
3120 notes: base32::encode_prefixed(FEDIMINT_PREFIX, &ecash),
3121 })
3122 } else {
3123 Err(AdminGatewayError::Unexpected(anyhow::anyhow!(
3124 "No mint module available"
3125 )))
3126 }
3127 }
3128
3129 async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> {
3132 let was_running = {
3136 let mut state_guard = self.state.write().await;
3137 if let GatewayState::Running { lightning_context } = state_guard.clone() {
3138 *state_guard = GatewayState::ShuttingDown { lightning_context };
3139 true
3140 } else {
3141 false
3142 }
3143 };
3144
3145 if was_running {
3153 self.federation_manager
3154 .read()
3155 .await
3156 .wait_for_incoming_payments()
3157 .await?;
3158 }
3159
3160 let tg = task_group.clone();
3161 tg.spawn("Kill Gateway", |_task_handle| async {
3162 if let Err(err) = task_group.shutdown_join_all(Duration::from_mins(3)).await {
3163 warn!(target: LOG_GATEWAY, err = %err.fmt_compact(), "Error shutting down gateway");
3164 }
3165 });
3166 Ok(())
3167 }
3168
3169 fn get_task_group(&self) -> TaskGroup {
3170 self.task_group.clone()
3171 }
3172
3173 async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> AdminResult<WithdrawResponse> {
3176 let WithdrawPayload {
3177 amount,
3178 address,
3179 federation_id,
3180 quoted_fees,
3181 } = payload;
3182
3183 let address_network = get_network_for_address(&address);
3184 let gateway_network = self.network;
3185 let Ok(address) = address.require_network(gateway_network) else {
3186 return Err(AdminGatewayError::WithdrawError {
3187 failure_reason: format!(
3188 "Gateway is running on network {gateway_network}, but provided withdraw address is for network {address_network}"
3189 ),
3190 });
3191 };
3192
3193 let client = self.select_client(federation_id).await?;
3194
3195 if let Ok(wallet_module) = client
3196 .value()
3197 .get_first_module::<fedimint_walletv2_client::WalletClientModule>()
3198 {
3199 let quoted_fee = quoted_fees.map(|fees| fees.amount());
3202 return withdraw_v2(client.value(), &wallet_module, &address, amount, quoted_fee).await;
3203 }
3204
3205 let wallet_module = client
3206 .value()
3207 .get_first_module::<WalletClientModule>()
3208 .map_err(anyhow::Error::from)?;
3209
3210 let (withdraw_amount, fees) = match quoted_fees {
3213 Some(fees) => {
3215 let amt = match amount {
3216 BitcoinAmountOrAll::Amount(a) => a,
3217 BitcoinAmountOrAll::All => {
3218 return Err(AdminGatewayError::WithdrawError {
3220 failure_reason:
3221 "Cannot use 'all' with quoted fees - amount must be resolved first"
3222 .to_string(),
3223 });
3224 }
3225 };
3226 (amt, fees)
3227 }
3228 None => match amount {
3230 BitcoinAmountOrAll::All => {
3235 let balance = client.value().get_balance_for_btc().await.map_err(|err| {
3236 AdminGatewayError::Unexpected(anyhow!(
3237 "Balance not available: {}",
3238 err.fmt_compact()
3239 ))
3240 })?;
3241
3242 wallet_module
3243 .max_withdrawable_amount(&address, balance)
3244 .await
3245 .map_err(|err| AdminGatewayError::WithdrawError {
3246 failure_reason: format!(
3247 "Insufficient funds. Balance: {balance}: {}",
3248 err.fmt_compact()
3249 ),
3250 })?
3251 }
3252 BitcoinAmountOrAll::Amount(amount) => (
3253 amount,
3254 wallet_module
3255 .get_withdraw_fees(&address, amount)
3256 .await
3257 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?,
3258 ),
3259 },
3260 };
3261
3262 let operation_id = wallet_module
3263 .withdraw(&address, withdraw_amount, fees, ())
3264 .await
3265 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?;
3266 let mut updates = wallet_module
3267 .subscribe_withdraw_updates(operation_id)
3268 .await
3269 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?
3270 .into_stream();
3271
3272 while let Some(update) = updates.next().await {
3273 match update {
3274 WithdrawState::Succeeded(txid) => {
3275 info!(target: LOG_GATEWAY, amount = %withdraw_amount, address = %address, "Sent funds");
3276 return Ok(WithdrawResponse { txid, fees });
3277 }
3278 WithdrawState::Failed(e) => {
3279 return Err(AdminGatewayError::WithdrawError { failure_reason: e });
3280 }
3281 WithdrawState::Created => {}
3282 }
3283 }
3284
3285 Err(AdminGatewayError::WithdrawError {
3286 failure_reason: "Ran out of state updates while withdrawing".to_string(),
3287 })
3288 }
3289
3290 async fn handle_withdraw_preview_msg(
3293 &self,
3294 payload: WithdrawPreviewPayload,
3295 ) -> AdminResult<WithdrawPreviewResponse> {
3296 let gateway_network = self.network;
3297 let address_checked = payload
3298 .address
3299 .clone()
3300 .require_network(gateway_network)
3301 .map_err(|_| AdminGatewayError::WithdrawError {
3302 failure_reason: "Address network mismatch".to_string(),
3303 })?;
3304
3305 let client = self.select_client(payload.federation_id).await?;
3306
3307 let WithdrawDetails {
3308 amount,
3309 mint_fees,
3310 peg_out_fees,
3311 flat_peg_out_fee,
3312 } = match payload.amount {
3313 BitcoinAmountOrAll::All => {
3314 calculate_max_withdrawable(client.value(), &address_checked).await?
3315 }
3316 BitcoinAmountOrAll::Amount(btc_amount) => {
3317 if let Ok(wallet_module) = client.value().get_first_module::<WalletClientModule>() {
3318 WithdrawDetails {
3319 amount: btc_amount.into(),
3320 mint_fees: None,
3321 peg_out_fees: wallet_module
3322 .get_withdraw_fees(&address_checked, btc_amount)
3323 .await
3324 .map_err(|e| AdminGatewayError::Unexpected(e.into()))?,
3325 flat_peg_out_fee: false,
3326 }
3327 } else if let Ok(wallet_module) = client
3328 .value()
3329 .get_first_module::<fedimint_walletv2_client::WalletClientModule>(
3330 ) {
3331 let fee = wallet_module.send_fee().await.map_err(|e| {
3332 AdminGatewayError::WithdrawError {
3333 failure_reason: e.fmt_compact().to_string(),
3334 }
3335 })?;
3336 let output_amount = btc_amount.checked_add(fee).ok_or_else(|| {
3339 AdminGatewayError::WithdrawError {
3340 failure_reason: "Withdrawal amount overflow".to_string(),
3341 }
3342 })?;
3343 let federation_fees = wallet_module
3344 .send_fee_quote(output_amount)
3345 .await
3346 .map_err(|e| AdminGatewayError::WithdrawError {
3347 failure_reason: e.fmt_compact().to_string(),
3348 })?
3349 .total()
3350 .get_bitcoin();
3351 WithdrawDetails {
3352 amount: btc_amount.into(),
3353 mint_fees: Some(federation_fees),
3354 peg_out_fees: PegOutFees::from_amount(fee),
3355 flat_peg_out_fee: true,
3356 }
3357 } else {
3358 return Err(AdminGatewayError::Unexpected(anyhow!(
3359 "No wallet module found"
3360 )));
3361 }
3362 }
3363 };
3364
3365 let total_cost = amount
3366 .checked_add(peg_out_fees.amount().into())
3367 .and_then(|a| a.checked_add(mint_fees.unwrap_or(Amount::ZERO)))
3368 .ok_or_else(|| AdminGatewayError::Unexpected(anyhow!("Total cost overflow")))?;
3369
3370 Ok(WithdrawPreviewResponse {
3371 withdraw_amount: amount,
3372 address: payload.address.assume_checked().to_string(),
3373 peg_out_fees,
3374 total_cost,
3375 mint_fees,
3376 flat_peg_out_fee,
3377 })
3378 }
3379
3380 async fn handle_payment_log_msg(
3392 &self,
3393 PaymentLogPayload {
3394 end_position,
3395 pagination_size,
3396 federation_id,
3397 event_kinds,
3398 }: PaymentLogPayload,
3399 ) -> AdminResult<PaymentLogResponse> {
3400 const BATCH_SIZE: u64 = 10_000;
3401 let federation_manager = self.federation_manager.read().await;
3402 let client = federation_manager
3403 .client(&federation_id)
3404 .ok_or(FederationNotConnected {
3405 federation_id_prefix: federation_id.to_prefix(),
3406 })?
3407 .value();
3408
3409 let event_kinds = if event_kinds.is_empty() {
3413 ALL_GATEWAY_EVENTS.to_vec()
3414 } else {
3415 event_kinds
3416 };
3417
3418 let end_position = if let Some(position) = end_position {
3419 position
3420 } else {
3421 let mut dbtx = client.db().begin_transaction_nc().await;
3422 dbtx.get_next_event_log_id().await
3423 };
3424
3425 let mut start_position = end_position.saturating_sub(BATCH_SIZE);
3426
3427 let mut payment_log = Vec::new();
3428
3429 while payment_log.len() < pagination_size {
3430 let batch = client.get_event_log(Some(start_position), BATCH_SIZE).await;
3431 let mut filtered_batch = batch
3432 .into_iter()
3433 .filter(|e| e.id() <= end_position && event_kinds.contains(&e.as_raw().kind))
3434 .collect::<Vec<_>>();
3435 filtered_batch.reverse();
3436 payment_log.extend(filtered_batch);
3437
3438 start_position = start_position.saturating_sub(BATCH_SIZE);
3440
3441 if start_position == EventLogId::LOG_START {
3442 break;
3443 }
3444 }
3445
3446 payment_log.truncate(pagination_size);
3448
3449 Ok(PaymentLogResponse(payment_log))
3450 }
3451
3452 async fn handle_set_mnemonic_msg(&self, payload: SetMnemonicPayload) -> AdminResult<()> {
3455 let mut state_guard = self.state.write().await;
3460
3461 let GatewayState::NotConfigured { mnemonic_sender } = state_guard.clone() else {
3463 return Err(AdminGatewayError::MnemonicError(anyhow!(
3464 "Gateway is not is NotConfigured state"
3465 )));
3466 };
3467
3468 let mnemonic = if let Some(words) = payload.words {
3469 info!(target: LOG_GATEWAY, "Using user provided mnemonic");
3470 Mnemonic::parse_in_normalized(Language::English, words.as_str()).map_err(|e| {
3471 AdminGatewayError::MnemonicError(anyhow!(format!(
3472 "Seed phrase provided in environment was invalid {e:?}"
3473 )))
3474 })?
3475 } else {
3476 debug!(target: LOG_GATEWAY, "Generating mnemonic and writing entropy to client storage");
3477 Bip39RootSecretStrategy::<12>::random(&mut OsRng)
3478 };
3479
3480 Client::store_encodable_client_secret(&self.gateway_db, mnemonic.to_entropy())
3481 .await
3482 .map_err(|err| AdminGatewayError::MnemonicError(err.into()))?;
3483
3484 *state_guard = GatewayState::Disconnected;
3485 drop(state_guard);
3486
3487 let _ = mnemonic_sender.send(());
3489
3490 Ok(())
3491 }
3492
3493 async fn handle_create_offer_for_operator_msg(
3495 &self,
3496 payload: CreateOfferPayload,
3497 ) -> AdminResult<CreateOfferResponse> {
3498 let lightning_context = self.get_lightning_context().await?;
3499 let offer = lightning_context.lnrpc.create_offer(
3500 payload.amount,
3501 payload.description,
3502 payload.expiry_secs,
3503 payload.quantity,
3504 )?;
3505 Ok(CreateOfferResponse { offer })
3506 }
3507
3508 async fn handle_pay_offer_for_operator_msg(
3510 &self,
3511 payload: PayOfferPayload,
3512 ) -> AdminResult<PayOfferResponse> {
3513 let lightning_context = self.get_lightning_context().await?;
3514 let preimage = lightning_context
3515 .lnrpc
3516 .pay_offer(
3517 payload.offer,
3518 payload.quantity,
3519 payload.amount,
3520 payload.payer_note,
3521 )
3522 .await?;
3523 Ok(PayOfferResponse {
3524 preimage: preimage.to_string(),
3525 })
3526 }
3527
3528 async fn handle_export_invite_codes(
3531 &self,
3532 ) -> BTreeMap<FederationId, BTreeMap<PeerId, (String, InviteCode)>> {
3533 let fed_manager = self.federation_manager.read().await;
3534 fed_manager.all_invite_codes().await
3535 }
3536
3537 async fn handle_get_note_summary_msg(
3540 &self,
3541 federation_id: &FederationId,
3542 ) -> AdminResult<TieredCounts> {
3543 let fed_manager = self.federation_manager.read().await;
3544 fed_manager.get_note_summary(federation_id).await
3545 }
3546
3547 fn get_password_hash(&self) -> String {
3548 self.bcrypt_password_hash.clone()
3549 }
3550
3551 fn gatewayd_version(&self) -> String {
3552 let gatewayd_version = env!("CARGO_PKG_VERSION");
3553 gatewayd_version.to_string()
3554 }
3555
3556 async fn get_chain_source(&self) -> (ChainSource, Network) {
3557 (self.chain_source.clone(), self.network)
3558 }
3559
3560 fn lightning_mode(&self) -> LightningMode {
3561 self.lightning_mode.clone()
3562 }
3563
3564 async fn is_configured(&self) -> bool {
3565 !matches!(self.get_state().await, GatewayState::NotConfigured { .. })
3566 }
3567}
3568
3569impl Gateway {
3571 async fn public_key_v2(&self, federation_id: &FederationId) -> Option<PublicKey> {
3575 self.federation_manager
3576 .read()
3577 .await
3578 .client(federation_id)
3579 .and_then(|client| {
3580 client
3583 .value()
3584 .get_first_module::<GatewayClientModuleV2>()
3585 .ok()
3586 .map(|module| module.keypair.public_key())
3587 })
3588 }
3589
3590 async fn receive_enabled(&self, federation_id: FederationId) -> bool {
3595 self.gateway_db
3596 .begin_transaction_nc()
3597 .await
3598 .load_federation_config(federation_id)
3599 .await
3600 .is_none_or(|config| config.receive_enabled())
3601 }
3602
3603 pub async fn routing_info_v2(
3606 &self,
3607 federation_id: &FederationId,
3608 ) -> Result<Option<RoutingInfo>> {
3609 let context = self.get_lightning_context().await?;
3610
3611 let mut dbtx = self.gateway_db.begin_transaction_nc().await;
3612 let fed_config = dbtx.load_federation_config(*federation_id).await.ok_or(
3613 PublicGatewayError::FederationNotConnected(FederationNotConnected {
3614 federation_id_prefix: federation_id.to_prefix(),
3615 }),
3616 )?;
3617
3618 let lightning_fee = fed_config.lightning_fee;
3619 let transaction_fee = fed_config.transaction_fee;
3620 let receive_enabled = fed_config.receive_enabled();
3621
3622 let send_fee_default = lightning_fee.checked_add(transaction_fee).ok_or_else(|| {
3625 PublicGatewayError::Unexpected(anyhow!(
3626 "The configured fees of federation {federation_id} cannot be added"
3627 ))
3628 })?;
3629
3630 Ok(self
3631 .public_key_v2(federation_id)
3632 .await
3633 .map(|module_public_key| RoutingInfo {
3634 lightning_public_key: context.lightning_public_key,
3635 lightning_alias: Some(context.lightning_alias.clone()),
3636 module_public_key,
3637 send_fee_default,
3638 send_fee_minimum: transaction_fee,
3642 expiration_delta_default: 1440,
3643 expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2,
3644 receive_fee: transaction_fee,
3647 receive_enabled,
3648 }))
3649 }
3650
3651 pub async fn send_payment_v2(
3654 &self,
3655 payload: SendPaymentPayload,
3656 ) -> Result<std::result::Result<[u8; 32], Signature>> {
3657 let client = self.select_client(payload.federation_id).await?;
3658 let module = client
3661 .value()
3662 .get_first_module::<GatewayClientModuleV2>()
3663 .map_err(|err| PublicGatewayError::LNv2(LNv2Error::OutgoingPayment(err.into())))?;
3664
3665 module
3666 .send_payment(payload)
3667 .await
3668 .map_err(|err| LNv2Error::OutgoingPayment(err.into()))
3669 .map_err(PublicGatewayError::LNv2)
3670 }
3671
3672 pub async fn create_bolt11_invoice_v2(
3677 &self,
3678 payload: CreateBolt11InvoicePayload,
3679 ) -> Result<Bolt11Invoice> {
3680 if !self.invoice_rate_limiter.try_acquire() {
3684 return Err(PublicGatewayError::RateLimited);
3685 }
3686
3687 let contract = payload.contract.clone();
3689
3690 if !fedimint_core::runtime::spawn_blocking(move || contract.verify()).await {
3691 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3692 "The contract is invalid".to_string(),
3693 )));
3694 }
3695
3696 let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or(
3697 LNv2Error::IncomingPayment(format!(
3698 "Federation {} does not exist",
3699 payload.federation_id
3700 )),
3701 )?;
3702
3703 if !payment_info.receive_enabled {
3704 return Err(PublicGatewayError::ReceiveDisabled {
3705 federation_id_prefix: payload.federation_id.to_prefix(),
3706 });
3707 }
3708
3709 if payload.contract.commitment.refund_pk != payment_info.module_public_key {
3710 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3711 "The incoming contract is keyed to another gateway".to_string(),
3712 )));
3713 }
3714
3715 let contract_amount = payment_info.receive_fee.subtract_from(payload.amount.msats);
3716
3717 if contract_amount == Amount::ZERO {
3718 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3719 "Zero amount incoming contracts are not supported".to_string(),
3720 )));
3721 }
3722
3723 if contract_amount != payload.contract.commitment.amount {
3724 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3725 "The contract amount does not pay the correct amount of fees".to_string(),
3726 )));
3727 }
3728
3729 if payload.contract.commitment.expiration_or_fee <= duration_since_epoch().as_secs() {
3730 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3731 "The contract has already expired".to_string(),
3732 )));
3733 }
3734
3735 if payload.expiry_secs > MAX_INVOICE_EXPIRY_SECS {
3736 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3737 "The invoice expiry exceeds the maximum of one day".to_string(),
3738 )));
3739 }
3740
3741 let payment_hash = match payload.contract.commitment.payment_image {
3742 PaymentImage::Hash(payment_hash) => payment_hash,
3743 PaymentImage::Point(..) => {
3744 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3745 "PaymentImage is not a payment hash".to_string(),
3746 )));
3747 }
3748 };
3749
3750 let mut dbtx = self.gateway_db.begin_transaction().await;
3755
3756 let invoice_expires_at_secs = duration_since_epoch()
3757 .as_secs()
3758 .saturating_add(u64::from(payload.expiry_secs));
3759
3760 if dbtx
3761 .save_registered_incoming_contract(
3762 payload.federation_id,
3763 payload.amount,
3764 invoice_expires_at_secs,
3765 payload.contract,
3766 )
3767 .await
3768 .is_some()
3769 {
3770 return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3771 "PaymentHash is already registered".to_string(),
3772 )));
3773 }
3774
3775 dbtx.commit_tx_result().await.map_err(|_| {
3776 PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3777 "Payment hash is already registered".to_string(),
3778 ))
3779 })?;
3780
3781 match self
3782 .create_invoice_via_lnrpc_v2(
3783 payment_hash,
3784 payload.amount,
3785 payload.description.clone(),
3786 payload.expiry_secs,
3787 )
3788 .await
3789 {
3790 Ok(invoice) => Ok(invoice),
3791 Err(err) => {
3792 let mut dbtx = self.gateway_db.begin_transaction().await;
3795 dbtx.delete_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3796 .await;
3797 if let Err(db_err) = dbtx.commit_tx_result().await {
3798 warn!(
3799 target: LOG_GATEWAY,
3800 err = %db_err.fmt_compact(),
3801 %payment_hash,
3802 "Failed to release incoming contract reservation after Lightning error"
3803 );
3804 }
3805
3806 Err(err.into())
3807 }
3808 }
3809 }
3810
3811 pub async fn create_invoice_via_lnrpc_v2(
3814 &self,
3815 payment_hash: sha256::Hash,
3816 amount: Amount,
3817 description: Bolt11InvoiceDescription,
3818 expiry_time: u32,
3819 ) -> std::result::Result<Bolt11Invoice, LightningRpcError> {
3820 let lnrpc = self.get_lightning_context().await?.lnrpc;
3821
3822 let response = match description {
3823 Bolt11InvoiceDescription::Direct(description) => {
3824 lnrpc
3825 .create_invoice(CreateInvoiceRequest {
3826 payment_hash: Some(payment_hash),
3827 amount_msat: amount.msats,
3828 expiry_secs: expiry_time,
3829 description: Some(InvoiceDescription::Direct(description)),
3830 })
3831 .await?
3832 }
3833 Bolt11InvoiceDescription::Hash(hash) => {
3834 lnrpc
3835 .create_invoice(CreateInvoiceRequest {
3836 payment_hash: Some(payment_hash),
3837 amount_msat: amount.msats,
3838 expiry_secs: expiry_time,
3839 description: Some(InvoiceDescription::Hash(hash)),
3840 })
3841 .await?
3842 }
3843 };
3844
3845 Bolt11Invoice::from_str(&response.invoice).map_err(|e| {
3846 LightningRpcError::FailedToGetInvoice {
3847 failure_reason: e.to_string(),
3848 }
3849 })
3850 }
3851
3852 pub async fn verify_bolt11_preimage_v2(
3853 &self,
3854 payment_hash: sha256::Hash,
3855 wait: bool,
3856 ) -> std::result::Result<VerifyResponse, String> {
3857 let registered_contract = self
3858 .gateway_db
3859 .begin_transaction_nc()
3860 .await
3861 .load_registered_incoming_contract(PaymentImage::Hash(payment_hash))
3862 .await
3863 .ok_or("Unknown payment hash".to_string())?;
3864
3865 let client = self
3866 .select_client(registered_contract.federation_id)
3867 .await
3868 .map_err(|_| "Not connected to federation".to_string())?
3869 .into_value();
3870
3871 let operation_id = OperationId::from_encodable(®istered_contract.contract);
3872
3873 if !(wait || client.operation_exists(operation_id).await) {
3874 return Ok(VerifyResponse::pending());
3875 }
3876
3877 let module = client
3878 .get_first_module::<GatewayClientModuleV2>()
3879 .expect("Must have client module");
3880
3881 let Ok(state) = timeout(VERIFY_WAIT_TIMEOUT, module.await_receive(operation_id)).await
3882 else {
3883 return Ok(VerifyResponse::pending());
3884 };
3885
3886 let preimage = match state {
3887 FinalReceiveState::Success(preimage) => Ok(preimage),
3888 FinalReceiveState::Failure => Err("Payment has failed".to_string()),
3889 FinalReceiveState::Refunded => Err("Payment has been refunded".to_string()),
3890 FinalReceiveState::Rejected => Err("Payment has been rejected".to_string()),
3891 }?;
3892
3893 Ok(VerifyResponse::settled(preimage))
3894 }
3895
3896 pub async fn get_registered_incoming_contract_and_client_v2(
3900 &self,
3901 payment_image: PaymentImage,
3902 amount_msats: u64,
3903 ) -> Result<(IncomingContract, ClientHandleArc)> {
3904 let registered_incoming_contract = self
3905 .gateway_db
3906 .begin_transaction_nc()
3907 .await
3908 .load_registered_incoming_contract(payment_image)
3909 .await
3910 .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment(
3911 "No corresponding decryption contract available".to_string(),
3912 )))?;
3913
3914 if registered_incoming_contract.incoming_amount_msats != amount_msats {
3915 return Err(PublicGatewayError::LNv2(
3916 LNv2Error::IncomingAmountMismatch {
3917 registered_msats: registered_incoming_contract.incoming_amount_msats,
3918 payment_msats: amount_msats,
3919 },
3920 ));
3921 }
3922
3923 if !self
3927 .receive_enabled(registered_incoming_contract.federation_id)
3928 .await
3929 {
3930 return Err(PublicGatewayError::ReceiveDisabled {
3931 federation_id_prefix: registered_incoming_contract.federation_id.to_prefix(),
3932 });
3933 }
3934
3935 let client = self
3936 .select_client(registered_incoming_contract.federation_id)
3937 .await?
3938 .into_value();
3939
3940 Ok((registered_incoming_contract.contract, client))
3941 }
3942
3943 async fn await_complete_htlc(
3952 &self,
3953 htlc: InterceptPaymentResponse,
3954 ) -> std::result::Result<(), LightningRpcError> {
3955 loop {
3956 let lightning_context = self.await_lightning_context().await;
3957
3958 match lightning_context.lnrpc.complete_htlc(htlc.clone()).await {
3959 Ok(()) => return Ok(()),
3960 Err(err @ LightningRpcError::HtlcCompletionRejected { .. }) => {
3961 warn!(
3962 target: LOG_GATEWAY,
3963 err = %err.fmt_compact(),
3964 "Lightning cannot reach the requested terminal HTLC outcome",
3965 );
3966 return Err(err);
3967 }
3968 Err(err) => {
3969 warn!(
3970 target: LOG_GATEWAY,
3971 err = %err.fmt_compact(),
3972 "Failure trying to complete HTLC, retrying",
3973 );
3974 }
3975 }
3976
3977 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
3978 }
3979 }
3980
3981 async fn await_outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
3990 loop {
3991 let lightning_context = self.await_lightning_context().await;
3992
3993 match lightning_context
3994 .lnrpc
3995 .outbound_payment_exists(payment_hash)
3996 .await
3997 {
3998 Ok(exists) => return exists,
3999 Err(err) => {
4000 warn!(
4001 target: LOG_GATEWAY,
4002 err = %err.fmt_compact(),
4003 %payment_hash,
4004 "Failed to check for a dispatched payment, retrying",
4005 );
4006 }
4007 }
4008
4009 sleep(LIGHTNING_CONTEXT_RETRY_INTERVAL).await;
4010 }
4011 }
4012}
4013
4014#[async_trait]
4015impl IGatewayClientV2 for Gateway {
4016 async fn complete_htlc(
4017 &self,
4018 htlc_response: InterceptPaymentResponse,
4019 ) -> std::result::Result<(), LightningRpcError> {
4020 self.await_complete_htlc(htlc_response).await
4021 }
4022
4023 async fn is_direct_swap(
4024 &self,
4025 invoice: &Bolt11Invoice,
4026 ) -> std::result::Result<Option<(IncomingContract, ClientHandleArc)>, GatewayClientV2Error>
4027 {
4028 let lightning_context = self.await_lightning_context().await;
4033 if lightning_context.lightning_public_key == invoice.get_payee_pub_key() {
4034 let (contract, client) = self
4035 .get_registered_incoming_contract_and_client_v2(
4036 PaymentImage::Hash(*invoice.payment_hash()),
4037 invoice
4038 .amount_milli_satoshis()
4039 .expect("The amount invoice has been previously checked"),
4040 )
4041 .await
4042 .map_err(GatewayClientV2Error::new)?;
4043 Ok(Some((contract, client)))
4044 } else {
4045 Ok(None)
4046 }
4047 }
4048
4049 async fn pay(
4050 &self,
4051 invoice: Bolt11Invoice,
4052 max_delay: u64,
4053 max_fee: Amount,
4054 ) -> std::result::Result<[u8; 32], LightningRpcError> {
4055 let lightning_context = self.await_lightning_context().await;
4058 lightning_context
4059 .lnrpc
4060 .pay(invoice, max_delay, max_fee)
4061 .await
4062 .map(|response| response.preimage.0)
4063 }
4064
4065 async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
4066 self.await_outbound_payment_exists(payment_hash).await
4067 }
4068
4069 async fn min_contract_amount(
4070 &self,
4071 federation_id: &FederationId,
4072 amount: u64,
4073 ) -> std::result::Result<Amount, GatewayClientV2Error> {
4074 Ok(self
4075 .routing_info_v2(federation_id)
4076 .await
4077 .map_err(GatewayClientV2Error::new)?
4078 .ok_or_else(|| GatewayClientV2Error::new("Routing Info not available"))?
4079 .send_fee_minimum
4080 .add_to(amount))
4081 }
4082
4083 async fn is_lnv1_invoice(&self, invoice: &Bolt11Invoice) -> Option<Spanned<ClientHandleArc>> {
4084 let rhints = invoice.route_hints();
4085 let hop = rhints.first().and_then(|rh| rh.0.last())?;
4086
4087 let lightning_context = self.await_lightning_context().await;
4090 if hop.src_node_id != lightning_context.lightning_public_key {
4091 return None;
4092 }
4093
4094 self.federation_manager
4095 .read()
4096 .await
4097 .get_client_for_index(hop.short_channel_id)
4098 }
4099
4100 async fn relay_lnv1_swap(
4101 &self,
4102 client: &ClientHandleArc,
4103 invoice: &Bolt11Invoice,
4104 allow_fresh_dispatch: bool,
4105 ) -> std::result::Result<Option<FinalReceiveState>, GatewayClientV2Error> {
4106 let swap_params =
4107 SwapParameters {
4108 payment_hash: *invoice.payment_hash(),
4109 amount_msat: Amount::from_msats(invoice.amount_milli_satoshis().ok_or_else(
4110 || GatewayClientV2Error::new("Amountless invoice not supported"),
4111 )?),
4112 };
4113 let lnv1 = client
4115 .get_first_module::<GatewayClientModule>()
4116 .map_err(|_| GatewayClientV2Error::new("Federation does not have an LNv1 module"))?;
4117 let Some(operation_id) = lnv1
4118 .gateway_handle_direct_swap(swap_params, allow_fresh_dispatch)
4119 .await
4120 .map_err(GatewayClientV2Error::new)?
4121 else {
4122 return Ok(None);
4123 };
4124 let mut stream = lnv1
4125 .gateway_subscribe_ln_receive(operation_id)
4126 .await
4127 .map_err(GatewayClientV2Error::new)?
4128 .into_stream();
4129 let mut final_state = FinalReceiveState::Failure;
4130 while let Some(update) = stream.next().await {
4131 match update {
4132 GatewayExtReceiveStates::Funding => {}
4133 GatewayExtReceiveStates::FundingFailed { error: _ } => {
4134 final_state = FinalReceiveState::Rejected;
4135 }
4136 GatewayExtReceiveStates::Preimage(preimage) => {
4137 final_state = FinalReceiveState::Success(preimage.0);
4138 }
4139 GatewayExtReceiveStates::RefundError {
4140 error_message: _,
4141 error: _,
4142 } => {
4143 final_state = FinalReceiveState::Failure;
4144 }
4145 GatewayExtReceiveStates::RefundSuccess {
4146 out_points: _,
4147 error: _,
4148 } => {
4149 final_state = FinalReceiveState::Refunded;
4150 }
4151 }
4152 }
4153
4154 Ok(Some(final_state))
4155 }
4156
4157 async fn claim_payment_image(
4158 &self,
4159 payment_image: &PaymentImage,
4160 operation_id: OperationId,
4161 ) -> bool {
4162 self.gateway_db
4166 .autocommit(
4167 |dbtx, _| {
4168 let payment_image = payment_image.clone();
4169 Box::pin(async move {
4170 let claimer = dbtx
4171 .claim_outgoing_payment_image(payment_image, operation_id)
4172 .await;
4173 Ok::<_, std::convert::Infallible>(claimer == operation_id)
4174 })
4175 },
4176 None,
4177 )
4178 .await
4179 .expect("Retries until the transaction commits")
4180 }
4181
4182 async fn is_lightning_connected(&self) -> bool {
4183 self.get_lightning_context().await.is_ok()
4184 }
4185
4186 async fn await_lightning_connected(&self) {
4187 self.await_lightning_context().await;
4188 }
4189}
4190
4191#[async_trait]
4192impl IGatewayClientV1 for Gateway {
4193 async fn claim_payment_image(
4194 &self,
4195 payment_image: &PaymentImage,
4196 operation_id: OperationId,
4197 ) -> bool {
4198 IGatewayClientV2::claim_payment_image(self, payment_image, operation_id).await
4199 }
4200
4201 async fn verify_preimage_authentication(
4202 &self,
4203 payment_hash: sha256::Hash,
4204 preimage_auth: sha256::Hash,
4205 contract: OutgoingContractAccount,
4206 ) -> std::result::Result<(), OutgoingPaymentError> {
4207 let mut dbtx = self.gateway_db.begin_transaction().await;
4208 if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await {
4209 if !PreimageAuth::new(secret_hash).verifies(preimage_auth) {
4210 return Err(OutgoingPaymentError {
4211 error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage,
4212 contract_id: contract.contract.contract_id(),
4213 contract: Some(contract),
4214 });
4215 }
4216 } else {
4217 dbtx.save_new_preimage_authentication(payment_hash, preimage_auth)
4220 .await;
4221 return dbtx
4222 .commit_tx_result()
4223 .await
4224 .map_err(|_| OutgoingPaymentError {
4225 error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid,
4226 contract_id: contract.contract.contract_id(),
4227 contract: Some(contract),
4228 });
4229 }
4230
4231 Ok(())
4232 }
4233
4234 async fn verify_pruned_invoice(
4235 &self,
4236 payment_data: PaymentData,
4237 ) -> std::result::Result<(), GatewayClientV1Error> {
4238 if matches!(payment_data, PaymentData::PrunedInvoice { .. }) {
4239 let lightning_context = self
4240 .get_lightning_context()
4241 .await
4242 .map_err(GatewayClientV1Error::new)?;
4243
4244 if !lightning_context.lnrpc.supports_private_payments() {
4245 return Err(GatewayClientV1Error::new(
4246 "Private payments are not supported by the lightning node",
4247 ));
4248 }
4249 }
4250
4251 Ok(())
4252 }
4253
4254 async fn get_routing_fees(&self, federation_id: FederationId) -> Option<RoutingFees> {
4255 let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await;
4256 let lightning_fee = gateway_dbtx
4257 .load_federation_config(federation_id)
4258 .await?
4259 .lightning_fee;
4260
4261 RoutingFees::try_from(lightning_fee)
4265 .inspect_err(|err| {
4266 warn!(
4267 target: LOG_GATEWAY,
4268 %federation_id,
4269 err = %err.fmt_compact(),
4270 "Configured lightning fee cannot be used. Set a smaller fee with `set_fees`."
4271 );
4272 })
4273 .ok()
4274 }
4275
4276 async fn get_client(&self, federation_id: &FederationId) -> Option<Spanned<ClientHandleArc>> {
4277 self.federation_manager
4278 .read()
4279 .await
4280 .client(federation_id)
4281 .cloned()
4282 }
4283
4284 async fn get_client_for_invoice(
4285 &self,
4286 payment_data: PaymentData,
4287 ) -> Option<Spanned<ClientHandleArc>> {
4288 let rhints = payment_data.route_hints();
4289 let hop = rhints.first().and_then(|rh| rh.0.last())?;
4290
4291 let lightning_context = self.await_lightning_context().await;
4294 if hop.src_node_id != lightning_context.lightning_public_key {
4295 return None;
4296 }
4297
4298 self.federation_manager
4299 .read()
4300 .await
4301 .get_client_for_index(hop.short_channel_id)
4302 }
4303
4304 async fn pay(
4305 &self,
4306 payment_data: PaymentData,
4307 max_delay: u64,
4308 max_fee: Amount,
4309 ) -> std::result::Result<PayInvoiceResponse, LightningRpcError> {
4310 let lightning_context = self.await_lightning_context().await;
4313
4314 match payment_data {
4315 PaymentData::Invoice(invoice) => {
4316 lightning_context
4317 .lnrpc
4318 .pay(invoice, max_delay, max_fee)
4319 .await
4320 }
4321 PaymentData::PrunedInvoice(invoice) => {
4322 lightning_context
4323 .lnrpc
4324 .pay_private(invoice, max_delay, max_fee)
4325 .await
4326 }
4327 }
4328 }
4329
4330 async fn outbound_payment_exists(&self, payment_hash: sha256::Hash) -> bool {
4331 self.await_outbound_payment_exists(payment_hash).await
4332 }
4333
4334 async fn complete_htlc(
4335 &self,
4336 htlc: InterceptPaymentResponse,
4337 ) -> std::result::Result<(), LightningRpcError> {
4338 self.await_complete_htlc(htlc).await
4339 }
4340
4341 async fn is_lnv2_direct_swap(
4342 &self,
4343 payment_hash: sha256::Hash,
4344 amount: Amount,
4345 ) -> std::result::Result<
4346 Option<(
4347 fedimint_lnv2_common::contracts::IncomingContract,
4348 ClientHandleArc,
4349 )>,
4350 GatewayClientV1Error,
4351 > {
4352 let (contract, client) = self
4353 .get_registered_incoming_contract_and_client_v2(
4354 PaymentImage::Hash(payment_hash),
4355 amount.msats,
4356 )
4357 .await
4358 .map_err(GatewayClientV1Error::new)?;
4359 Ok(Some((contract, client)))
4360 }
4361}