diff --git a/gateway/ln-gateway/src/client.rs b/gateway/ln-gateway/src/client.rs index 4f8fc360d65..fadc5e5734b 100644 --- a/gateway/ln-gateway/src/client.rs +++ b/gateway/ln-gateway/src/client.rs @@ -16,9 +16,10 @@ use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped}; use fedimint_core::module::registry::ModuleDecoderRegistry; use crate::db::FederationConfig; +use crate::error::AdminGatewayError; use crate::gateway_module_v2::GatewayClientInitV2; use crate::state_machine::GatewayClientInit; -use crate::{Gateway, GatewayError, Result}; +use crate::{AdminResult, Gateway}; #[derive(Debug, Clone)] pub struct GatewayClientBuilder { @@ -42,8 +43,10 @@ impl GatewayClientBuilder { /// Reads a plain root secret from a database to construct a database. /// Only used for "legacy" federations before v0.5.0 - async fn client_plainrootsecret(&self, db: &Database) -> Result { - let client_secret = Client::load_decodable_client_secret::<[u8; 64]>(db).await?; + async fn client_plainrootsecret(&self, db: &Database) -> AdminResult { + let client_secret = Client::load_decodable_client_secret::<[u8; 64]>(db) + .await + .map_err(AdminGatewayError::ClientCreationError)?; Ok(PlainRootSecretStrategy::to_root_secret(&client_secret)) } @@ -54,7 +57,7 @@ impl GatewayClientBuilder { db: Database, federation_config: &FederationConfig, gateway: Arc, - ) -> Result { + ) -> AdminResult { let FederationConfig { federation_index, timelock_delta, @@ -75,7 +78,7 @@ impl GatewayClientBuilder { let mut client_builder = Client::builder(db) .await - .map_err(GatewayError::DatabaseError)?; + .map_err(AdminGatewayError::ClientCreationError)?; client_builder.with_module_inits(registry); client_builder.with_primary_module(self.primary_module); client_builder.with_connector(connector); @@ -91,11 +94,12 @@ impl GatewayClientBuilder { config: FederationConfig, gateway: Arc, mnemonic: &Mnemonic, - ) -> Result<()> { + ) -> AdminResult<()> { let client_config = config .connector .download_from_invite_code(&config.invite_code) - .await?; + .await + .map_err(AdminGatewayError::ClientCreationError)?; let federation_id = config.invite_code.federation_id(); let db = gateway .gateway_db @@ -110,7 +114,8 @@ impl GatewayClientBuilder { &client_config, config.invite_code.api_secret(), ) - .await?; + .await + .map_err(AdminGatewayError::ClientCreationError)?; let client = client_builder .recover( secret.clone(), @@ -120,8 +125,11 @@ impl GatewayClientBuilder { ) .await .map(Arc::new) - .map_err(GatewayError::ClientStateMachineError)?; - client.wait_for_all_recoveries().await?; + .map_err(AdminGatewayError::ClientCreationError)?; + client + .wait_for_all_recoveries() + .await + .map_err(AdminGatewayError::ClientCreationError)?; Ok(()) } @@ -132,15 +140,14 @@ impl GatewayClientBuilder { config: FederationConfig, gateway: Arc, mnemonic: &Mnemonic, - ) -> Result { + ) -> AdminResult { let invite_code = config.invite_code.clone(); let federation_id = invite_code.federation_id(); let db_path = self.work_dir.join(format!("{federation_id}.db")); let (db, root_secret) = if db_path.exists() { - let rocksdb = fedimint_rocksdb::RocksDb::open(db_path.clone()).map_err(|e| { - GatewayError::DatabaseError(anyhow::anyhow!("Error opening rocksdb: {e:?}")) - })?; + let rocksdb = fedimint_rocksdb::RocksDb::open(db_path.clone()) + .map_err(AdminGatewayError::ClientCreationError)?; let db = Database::new(rocksdb, ModuleDecoderRegistry::default()); let root_secret = self.client_plainrootsecret(&db).await?; (db, root_secret) @@ -162,24 +169,25 @@ impl GatewayClientBuilder { let client_config = config .connector .download_from_invite_code(&invite_code) - .await?; + .await + .map_err(AdminGatewayError::ClientCreationError)?; client_builder .join(root_secret, client_config.clone(), invite_code.api_secret()) .await } .map(Arc::new) - .map_err(GatewayError::ClientStateMachineError) + .map_err(AdminGatewayError::ClientCreationError) } /// Verifies that the saved `ClientConfig` contains the expected /// federation's config. - async fn verify_client_config(db: &Database, federation_id: FederationId) -> Result<()> { + async fn verify_client_config(db: &Database, federation_id: FederationId) -> AdminResult<()> { let mut dbtx = db.begin_transaction_nc().await; if let Some(config) = dbtx.get_value(&ClientConfigKey).await { if config.calculate_federation_id() != federation_id { - return Err(GatewayError::ClientCreationError( - "Federation Id did not match saved federation ID".to_string(), - )); + return Err(AdminGatewayError::ClientCreationError(anyhow::anyhow!( + "Federation Id did not match saved federation ID".to_string() + ))); } } Ok(()) diff --git a/gateway/ln-gateway/src/error.rs b/gateway/ln-gateway/src/error.rs new file mode 100644 index 00000000000..7e271d6199f --- /dev/null +++ b/gateway/ln-gateway/src/error.rs @@ -0,0 +1,144 @@ +use std::fmt::Display; + +use axum::response::{IntoResponse, Response}; +use fedimint_core::config::{FederationId, FederationIdPrefix}; +use fedimint_core::fmt_utils::OptStacktrace; +use reqwest::StatusCode; +use thiserror::Error; +use tracing::error; + +use crate::lightning::LightningRpcError; +use crate::state_machine::pay::OutgoingPaymentError; + +/// Errors that unauthenticated endpoints can encounter. For privacy reasons, +/// the error messages are intended to be redacted before returning to the +/// client. +#[derive(Debug, Error)] +pub enum PublicGatewayError { + #[error("Lightning rpc error: {}", .0)] + Lightning(#[from] LightningRpcError), + #[error("LNv1 error: {:?}", .0)] + LNv1(#[from] LNv1Error), + #[error("LNv2 error: {:?}", .0)] + LNv2(#[from] LNv2Error), + #[error("{}", .0)] + FederationNotConnected(#[from] FederationNotConnected), + #[error("Failed to receive ecash: {failure_reason}")] + ReceiveEcashError { failure_reason: String }, +} + +impl IntoResponse for PublicGatewayError { + fn into_response(self) -> Response { + // For privacy reasons, we do not return too many details about the failure of + // the request back to the client to prevent malicious clients from + // deducing state about the gateway/lightning node. + error!("{self}"); + let (error_message, status_code) = match self { + PublicGatewayError::FederationNotConnected(e) => { + (e.to_string(), StatusCode::BAD_REQUEST) + } + PublicGatewayError::ReceiveEcashError { .. } => ( + "Failed to receive ecash".to_string(), + StatusCode::INTERNAL_SERVER_ERROR, + ), + PublicGatewayError::Lightning(_) => ( + "Lightning Network operation failed".to_string(), + StatusCode::INTERNAL_SERVER_ERROR, + ), + PublicGatewayError::LNv1(_) => ( + "LNv1 operation failed, please contact gateway operator".to_string(), + StatusCode::INTERNAL_SERVER_ERROR, + ), + PublicGatewayError::LNv2(_) => ( + "LNv2 operation failed, please contact gateway operator".to_string(), + StatusCode::INTERNAL_SERVER_ERROR, + ), + }; + + Response::builder() + .status(status_code) + .body(error_message.into()) + .expect("Failed to create Response") + } +} + +/// Errors that authenticated endpoints can encounter. Full error message and +/// error details are returned to the admin client for debugging purposes. +#[derive(Debug, Error)] +pub enum AdminGatewayError { + #[error("Failed to create a federation client: {}", OptStacktrace(.0))] + ClientCreationError(anyhow::Error), + #[error("Failed to remove a federation client: {}", OptStacktrace(.0))] + ClientRemovalError(String), + #[error("There was an error with the Gateway's mnemonic: {}", OptStacktrace(.0))] + MnemonicError(anyhow::Error), + #[error("Unexpected Error: {}", OptStacktrace(.0))] + Unexpected(#[from] anyhow::Error), + #[error("{}", .0)] + FederationNotConnected(#[from] FederationNotConnected), + #[error("Error configuring the gateway: {}", OptStacktrace(.0))] + GatewayConfigurationError(String), + #[error("Lightning error: {}", OptStacktrace(.0))] + Lightning(#[from] LightningRpcError), + #[error("Error registering federation {federation_id}")] + RegistrationError { federation_id: FederationId }, + #[error("Error withdrawing funds onchain: {failure_reason}")] + WithdrawError { failure_reason: String }, +} + +impl IntoResponse for AdminGatewayError { + // For admin errors, always pass along the full error message for debugging + // purposes + fn into_response(self) -> Response { + error!("{self}"); + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(self.to_string().into()) + .expect("Failed to create Response") + } +} + +/// Errors that can occur during the LNv1 protocol. LNv1 errors are public and +/// the error messages should be redacted for privacy reasons. +#[derive(Debug, Error)] +pub enum LNv1Error { + #[error("Incoming payment error: {}", OptStacktrace(.0))] + IncomingPayment(String), + #[error( + "Outgoing Contract Error Reason: {message} Stack: {}", + OptStacktrace(error) + )] + OutgoingContract { + error: Box, + message: String, + }, + #[error("Outgoing Payment Error: {}", OptStacktrace(.0))] + OutgoingPayment(#[from] anyhow::Error), +} + +/// Errors that can occur during the LNv2 protocol. LNv2 errors are public and +/// the error messages should be redacted for privacy reasons. +#[derive(Debug, Error)] +pub enum LNv2Error { + #[error("Incoming Payment Error: {}", .0)] + IncomingPayment(String), + #[error("Outgoing Payment Error: {}", OptStacktrace(.0))] + OutgoingPayment(#[from] anyhow::Error), +} + +/// Public error that indicates the requested federation is not connected to +/// this gateway. +#[derive(Debug, Error)] +pub struct FederationNotConnected { + pub federation_id_prefix: FederationIdPrefix, +} + +impl Display for FederationNotConnected { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "No federation available for prefix {}", + self.federation_id_prefix + ) + } +} diff --git a/gateway/ln-gateway/src/federation_manager.rs b/gateway/ln-gateway/src/federation_manager.rs index 0789ba24c7f..736f2090550 100644 --- a/gateway/ln-gateway/src/federation_manager.rs +++ b/gateway/ln-gateway/src/federation_manager.rs @@ -10,10 +10,11 @@ use fedimint_core::util::Spanned; use tracing::info; use crate::db::GatewayDbtxNcExt; +use crate::error::{AdminGatewayError, FederationNotConnected}; use crate::gateway_module_v2::GatewayClientModuleV2; use crate::rpc::FederationInfo; use crate::state_machine::GatewayClientModule; -use crate::{GatewayError, Result}; +use crate::AdminResult; /// The first index that the gateway will assign to a federation. /// Note: This starts at 1 because LNv1 uses the `federation_index` as an SCID. @@ -61,7 +62,7 @@ impl FederationManager { &mut self, federation_id: FederationId, dbtx: &mut DatabaseTransaction<'_, NonCommittable>, - ) -> Result { + ) -> AdminResult { let federation_info = self.federation_info(federation_id, dbtx).await?; let gateway_keypair = dbtx.load_gateway_keypair_assert_exists().await; @@ -74,13 +75,13 @@ impl FederationManager { Ok(federation_info) } - async fn remove_client(&mut self, federation_id: FederationId) -> Result<()> { + async fn remove_client(&mut self, federation_id: FederationId) -> AdminResult<()> { let client = self .clients .remove(&federation_id) - .ok_or(GatewayError::InvalidMetadata(format!( - "No federation with id {federation_id}" - )))? + .ok_or(FederationNotConnected { + federation_id_prefix: federation_id.to_prefix(), + })? .into_value(); self.index_to_federation @@ -90,15 +91,15 @@ impl FederationManager { client.shutdown().await; Ok(()) } else { - Err(GatewayError::UnexpectedState( - "Federation client is not unique, failed to shutdown client".to_string(), - )) + Err(AdminGatewayError::ClientRemovalError(format!( + "Federation client {federation_id} is not unique, failed to shutdown client" + ))) } } /// Waits for ongoing incoming LNv1 and LNv2 payments to complete before /// returning. - pub async fn wait_for_incoming_payments(&self) -> Result<()> { + pub async fn wait_for_incoming_payments(&self) -> AdminResult<()> { for client in self.clients.values() { let active_operations = client.value().get_active_operations().await; let operation_log = client.value().operation_log(); @@ -129,13 +130,13 @@ impl FederationManager { &self, federation_id: FederationId, gateway_keypair: KeyPair, - ) -> Result<()> { + ) -> AdminResult<()> { let client = self .clients .get(&federation_id) - .ok_or(GatewayError::InvalidMetadata(format!( - "No federation with id {federation_id}" - )))?; + .ok_or(FederationNotConnected { + federation_id_prefix: federation_id.to_prefix(), + })?; client .value() @@ -212,11 +213,11 @@ impl FederationManager { &self, federation_id: FederationId, dbtx: &mut DatabaseTransaction<'_, NonCommittable>, - ) -> Result { + ) -> std::result::Result { let Some(federation_index) = self.get_index_for_federation(federation_id) else { - return Err(GatewayError::InvalidMetadata(format!( - "No federation with id {federation_id}" - ))); + return Err(FederationNotConnected { + federation_id_prefix: federation_id.to_prefix(), + }); }; self.clients @@ -269,13 +270,13 @@ impl FederationManager { pub async fn get_federation_config( &self, federation_id: FederationId, - ) -> Result { + ) -> AdminResult { let client = self .clients .get(&federation_id) - .ok_or(GatewayError::InvalidMetadata(format!( - "No federation with id {federation_id}" - )))?; + .ok_or(FederationNotConnected { + federation_id_prefix: federation_id.to_prefix(), + })?; Ok(client .borrow() .with(|client| client.get_config_json()) @@ -301,12 +302,12 @@ impl FederationManager { self.next_index.store(next_index, Ordering::SeqCst); } - pub fn pop_next_index(&self) -> Result { + pub fn pop_next_index(&self) -> AdminResult { let next_index = self.next_index.fetch_add(1, Ordering::Relaxed); // Check for overflow. if next_index == INITIAL_INDEX.wrapping_sub(1) { - return Err(GatewayError::GatewayConfigurationError( + return Err(AdminGatewayError::GatewayConfigurationError( "Federation Index overflow".to_string(), )); } diff --git a/gateway/ln-gateway/src/lib.rs b/gateway/ln-gateway/src/lib.rs index 6bff906024c..dbd9c4f3e9d 100644 --- a/gateway/ln-gateway/src/lib.rs +++ b/gateway/ln-gateway/src/lib.rs @@ -17,6 +17,7 @@ pub mod client; mod config; mod db; pub mod envs; +mod error; mod federation_manager; pub mod gateway_module_v2; pub mod lightning; @@ -28,7 +29,6 @@ pub mod gateway_lnrpc { tonic::include_proto!("gateway_lnrpc"); } -use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; use std::env; use std::fmt::Display; @@ -38,8 +38,6 @@ use std::sync::Arc; use std::time::Duration; use anyhow::anyhow; -use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; use bip39::Mnemonic; use bitcoin::{Address, Network, Txid}; use bitcoin_hashes::{sha256, Hash}; @@ -50,9 +48,9 @@ pub use config::GatewayParameters; use db::{ GatewayConfiguration, GatewayConfigurationKey, GatewayDbtxNcExt, GATEWAYD_DATABASE_VERSION, }; +use error::FederationNotConnected; use federation_manager::FederationManager; use fedimint_api_client::api::net::Connector; -use fedimint_api_client::api::FederationError; use fedimint_bip39::Bip39RootSecretStrategy; use fedimint_client::module::init::ClientModuleInitRegistry; use fedimint_client::secret::RootSecretStrategy; @@ -63,8 +61,6 @@ use fedimint_core::core::{ LEGACY_HARDCODED_INSTANCE_ID_WALLET, }; use fedimint_core::db::{apply_migrations_server, Database, DatabaseTransaction, DatabaseValue}; -use fedimint_core::endpoint_constants::REGISTER_GATEWAY_ENDPOINT; -use fedimint_core::fmt_utils::OptStacktrace; use fedimint_core::invite_code::InviteCode; use fedimint_core::module::CommonModuleInit; use fedimint_core::secp256k1::schnorr::Signature; @@ -100,14 +96,13 @@ use rpc::{ ReceiveEcashPayload, ReceiveEcashResponse, SetConfigurationPayload, SpendEcashPayload, SpendEcashResponse, SyncToChainPayload, V1_API_ENDPOINT, }; -use state_machine::pay::OutgoingPaymentError; use state_machine::{GatewayClientModule, GatewayExtPayStates}; -use thiserror::Error; use tokio::sync::RwLock; use tracing::{debug, error, info, info_span, warn, Instrument}; use crate::db::{get_gatewayd_database_migrations, FederationConfig}; use crate::envs::FM_GATEWAY_MNEMONIC_ENV; +use crate::error::{AdminGatewayError, LNv1Error, LNv2Error, PublicGatewayError}; use crate::gateway_lnrpc::create_invoice_request::Description; use crate::gateway_lnrpc::intercept_htlc_response::Forward; use crate::gateway_lnrpc::CreateInvoiceRequest; @@ -143,7 +138,8 @@ pub const DEFAULT_FEES: RoutingFees = RoutingFees { /// LNv2 CLTV Delta in blocks const EXPIRATION_DELTA_MINIMUM_V2: u64 = 144; -pub type Result = std::result::Result; +pub type Result = std::result::Result; +pub type AdminResult = std::result::Result; /// Name of the gateway's database that is used for metadata and configuration /// storage. @@ -662,8 +658,8 @@ impl Gateway { async fn try_handle_htlc_ln_legacy(&self, htlc_request: &InterceptHtlcRequest) -> Result<()> { // Check if the HTLC corresponds to a federation supporting legacy Lightning. let Some(federation_index) = htlc_request.short_channel_id else { - return Err(GatewayError::IncomingLNv1PaymentError(anyhow::anyhow!( - "Incoming payment has no last hop short channel id" + return Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment( + "Incoming payment has not last hop short channel id".to_string(), ))); }; @@ -675,9 +671,7 @@ impl Gateway { .await .get_client_for_index(federation_index) else { - return Err(GatewayError::IncomingLNv1PaymentError(anyhow::anyhow!( - "Incoming payment has a last hop short channel id that does not map to a known federation" - ))); + 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()))); }; client @@ -691,17 +685,16 @@ impl Gateway { .gateway_handle_intercepted_htlc(htlc) .await { - Ok(_) => return Ok(()), - Err(e) => { - error!("Got error intercepting HTLC: {e:?}, will retry..."); - } + Ok(_) => Ok(()), + Err(e) => Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment( + format!("Error intercepting HTLC {e:?}"), + ))), } } else { - error!("Got no HTLC result"); + Err(PublicGatewayError::LNv1(LNv1Error::IncomingPayment( + "Could not convert InterceptHtlcResult into an HTLC".to_string(), + ))) } - Err(GatewayError::IncomingLNv1PaymentError(anyhow::anyhow!( - "Incoming payment could not be handled" - ))) }) .await } @@ -731,7 +724,7 @@ impl Gateway { /// Returns information about the Gateway back to the client when requested /// via the webserver. - pub async fn handle_get_info(&self) -> Result { + pub async fn handle_get_info(&self) -> AdminResult { let GatewayState::Running { lightning_context } = self.get_state().await else { return Ok(GatewayInfo { federations: vec![], @@ -797,7 +790,7 @@ impl Gateway { pub async fn handle_get_federation_config( &self, federation_id_or: Option, - ) -> Result { + ) -> AdminResult { if !matches!(self.get_state().await, GatewayState::Running { .. }) { return Ok(GatewayFedConfig { federations: BTreeMap::new(), @@ -828,7 +821,7 @@ impl Gateway { /// Returns the balance of the requested federation that the Gateway is /// connected to. - pub async fn handle_balance_msg(&self, payload: BalancePayload) -> Result { + pub async fn handle_balance_msg(&self, payload: BalancePayload) -> AdminResult { // no need for instrument, it is done on api layer Ok(self .select_client(payload.federation_id) @@ -840,7 +833,7 @@ impl Gateway { /// Returns a Bitcoin deposit on-chain address for pegging in Bitcoin for a /// specific connected federation. - pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> Result
{ + pub async fn handle_address_msg(&self, payload: DepositAddressPayload) -> AdminResult
{ let (_, address, _) = self .select_client(payload.federation_id) .await? @@ -854,7 +847,7 @@ impl Gateway { /// Returns a Bitcoin TXID from a peg-out transaction for a specific /// connected federation. - pub async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> Result { + pub async fn handle_withdraw_msg(&self, payload: WithdrawPayload) -> AdminResult { let WithdrawPayload { amount, address, @@ -875,7 +868,11 @@ impl Gateway { .await?; let withdraw_amount = balance.checked_sub(fees.amount()); if withdraw_amount.is_none() { - return Err(GatewayError::InsufficientFunds); + return Err(AdminGatewayError::WithdrawError { + failure_reason: format!( + "Insufficient funds. Balance: {balance} Fees: {fees:?}" + ), + }); } (withdraw_amount.unwrap(), fees) } @@ -905,15 +902,15 @@ impl Gateway { return Ok(txid); } WithdrawState::Failed(e) => { - return Err(GatewayError::UnexpectedState(e)); + return Err(AdminGatewayError::WithdrawError { failure_reason: e }); } WithdrawState::Created => {} } } - Err(GatewayError::UnexpectedState( - "Ran out of state updates while withdrawing".to_string(), - )) + Err(AdminGatewayError::WithdrawError { + failure_reason: "Ran out of state updates while withdrawing".to_string(), + }) } /// Creates an invoice that is directly payable to the gateway's lightning @@ -923,7 +920,9 @@ impl Gateway { payload: CreateInvoiceForSelfPayload, ) -> Result { let GatewayState::Running { lightning_context } = self.get_state().await else { - return Err(GatewayError::Disconnected); + return Err(PublicGatewayError::Lightning( + LightningRpcError::FailedToConnect, + )); }; Bolt11Invoice::from_str( @@ -939,24 +938,32 @@ impl Gateway { .await? .invoice, ) - .map_err(|e| GatewayError::UnexpectedState(e.to_string())) + .map_err(|e| { + PublicGatewayError::Lightning(LightningRpcError::InvalidMetadata { + failure_reason: e.to_string(), + }) + }) } /// Requests the gateway to pay an outgoing LN invoice using its own funds. /// Returns the payment hash's preimage on success. - async fn handle_pay_invoice_self_msg(&self, payload: PayInvoicePayload) -> Result { - if let GatewayState::Running { lightning_context } = self.get_state().await { - let res = lightning_context - .lnrpc - .pay(payload.invoice, payload.max_delay, payload.max_fee) - .await?; - Ok(Preimage( - res.preimage.try_into().expect("preimage is 32 bytes"), - )) - } else { - warn!("Gateway is not connected to lightning node, cannot pay invoice"); - Err(GatewayError::Disconnected) - } + async fn handle_pay_invoice_self_msg( + &self, + payload: PayInvoicePayload, + ) -> AdminResult { + let GatewayState::Running { lightning_context } = self.get_state().await else { + return Err(AdminGatewayError::Lightning( + LightningRpcError::FailedToConnect, + )); + }; + + let res = lightning_context + .lnrpc + .pay(payload.invoice, payload.max_delay, payload.max_fee) + .await?; + Ok(Preimage( + res.preimage.try_into().expect("preimage is 32 bytes"), + )) } /// Requests the gateway to pay an outgoing LN invoice on behalf of a @@ -965,49 +972,63 @@ impl Gateway { &self, payload: fedimint_ln_client::pay::PayInvoicePayload, ) -> Result { - if let GatewayState::Running { .. } = self.get_state().await { - debug!("Handling pay invoice message: {payload:?}"); - let client = self.select_client(payload.federation_id).await?; - let contract_id = payload.contract_id; - let gateway_module = &client.value().get_first_module::()?; - let operation_id = gateway_module.gateway_pay_bolt11_invoice(payload).await?; - let mut updates = gateway_module - .gateway_subscribe_ln_pay(operation_id) - .await? - .into_stream(); - while let Some(update) = updates.next().await { - match update { - GatewayExtPayStates::Success { preimage, .. } => { - debug!("Successfully paid invoice: {contract_id}"); - return Ok(preimage); - } - GatewayExtPayStates::Fail { - error, - error_message, - } => { - error!("{error_message} while paying invoice: {contract_id}"); - return Err(GatewayError::OutgoingPaymentError(Box::new(error))); - } - GatewayExtPayStates::Canceled { error } => { - error!("Cancelled with {error} while paying invoice: {contract_id}"); - return Err(GatewayError::OutgoingPaymentError(Box::new(error))); - } - GatewayExtPayStates::Created => { - debug!("Got initial state Created while paying invoice: {contract_id}"); - } - other => { - info!("Got state {other:?} while paying invoice: {contract_id}"); - } - }; - } - - return Err(GatewayError::UnexpectedState( - "Ran out of state updates while paying invoice".to_string(), + let GatewayState::Running { .. } = self.get_state().await else { + return Err(PublicGatewayError::Lightning( + LightningRpcError::FailedToConnect, )); + }; + + debug!("Handling pay invoice message: {payload:?}"); + let client = self.select_client(payload.federation_id).await?; + let contract_id = payload.contract_id; + let gateway_module = &client + .value() + .get_first_module::() + .map_err(LNv1Error::OutgoingPayment) + .map_err(PublicGatewayError::LNv1)?; + let operation_id = gateway_module + .gateway_pay_bolt11_invoice(payload) + .await + .map_err(LNv1Error::OutgoingPayment) + .map_err(PublicGatewayError::LNv1)?; + let mut updates = gateway_module + .gateway_subscribe_ln_pay(operation_id) + .await + .map_err(LNv1Error::OutgoingPayment) + .map_err(PublicGatewayError::LNv1)? + .into_stream(); + while let Some(update) = updates.next().await { + match update { + GatewayExtPayStates::Success { preimage, .. } => { + debug!("Successfully paid invoice: {contract_id}"); + return Ok(preimage); + } + GatewayExtPayStates::Fail { + error, + error_message, + } => { + return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract { + error: Box::new(error), + message: format!( + "{error_message} while paying invoice with contract id {contract_id}" + ), + })); + } + GatewayExtPayStates::Canceled { error } => { + return Err(PublicGatewayError::LNv1(LNv1Error::OutgoingContract { error: Box::new(error.clone()), message: format!("Cancelled with {error} while paying invoice with contract id {contract_id}") })); + } + GatewayExtPayStates::Created => { + debug!("Got initial state Created while paying invoice: {contract_id}"); + } + other => { + info!("Got state {other:?} while paying invoice: {contract_id}"); + } + }; } - warn!("Gateway is not connected, cannot handle {payload:?}"); - Err(GatewayError::Disconnected) + Err(PublicGatewayError::LNv1(LNv1Error::OutgoingPayment( + anyhow!("Ran out of state updates while paying invoice"), + ))) } /// Handles a connection request to join a new federation. The gateway will @@ -1017,13 +1038,17 @@ impl Gateway { pub async fn handle_connect_federation( &self, payload: ConnectFedPayload, - ) -> Result { + ) -> AdminResult { let GatewayState::Running { lightning_context } = self.get_state().await else { - return Err(GatewayError::Disconnected); + return Err(AdminGatewayError::Lightning( + LightningRpcError::FailedToConnect, + )); }; let invite_code = InviteCode::from_str(&payload.invite_code).map_err(|e| { - GatewayError::InvalidMetadata(format!("Invalid federation member string {e:?}")) + AdminGatewayError::ClientCreationError(anyhow!(format!( + "Invalid federation member string {e:?}" + ))) })?; #[cfg(feature = "tor")] @@ -1047,7 +1072,9 @@ impl Gateway { // Check if this federation has already been registered if federation_manager.has_federation(federation_id) { - return Err(GatewayError::FederationAlreadyConnected); + return Err(AdminGatewayError::ClientCreationError(anyhow!( + "Federation has already been registered" + ))); } // `GatewayConfiguration` should always exist in the database when we are in the @@ -1120,9 +1147,7 @@ impl Gateway { let mut dbtx = self.gateway_db.begin_transaction().await; dbtx.save_federation_config(&federation_config).await; - dbtx.commit_tx_result() - .await - .map_err(GatewayError::DatabaseError)?; + dbtx.commit_tx().await; debug!("Federation with ID: {federation_id} connected and assigned federation index: {federation_index}"); Ok(federation_info) @@ -1135,7 +1160,7 @@ impl Gateway { pub async fn handle_leave_federation( &self, payload: LeaveFedPayload, - ) -> Result { + ) -> AdminResult { // Lock the federation manager before starting the db transaction to reduce the // chance of db write conflicts. let mut federation_manager = self.federation_manager.write().await; @@ -1146,9 +1171,7 @@ impl Gateway { .await?; dbtx.remove_federation_config(payload.federation_id).await; - dbtx.commit_tx_result() - .await - .map_err(GatewayError::DatabaseError)?; + dbtx.commit_tx().await; Ok(federation_info) } @@ -1157,12 +1180,12 @@ impl Gateway { pub async fn handle_backup_msg( &self, BackupPayload { federation_id }: BackupPayload, - ) -> Result<()> { + ) -> AdminResult<()> { let federation_manager = self.federation_manager.read().await; let client = federation_manager .client(&federation_id) - .ok_or(GatewayError::ClientCreationError(format!( - "Gateway does has not connected to {federation_id}" + .ok_or(AdminGatewayError::ClientCreationError(anyhow::anyhow!( + format!("Gateway has not connected to {federation_id}") )))? .value(); let metadata: BTreeMap = BTreeMap::new(); @@ -1177,7 +1200,7 @@ impl Gateway { /// Handles an authenticated request for the gateway's mnemonic. This also /// returns a vector of federations that are not using the mnemonic /// backup strategy. - pub async fn handle_mnemonic_msg(&self) -> Result { + pub async fn handle_mnemonic_msg(&self) -> AdminResult { let mnemonic = Self::load_or_generate_mnemonic(&self.gateway_db).await?; let words = mnemonic .word_iter() @@ -1214,12 +1237,12 @@ impl Gateway { routing_fees, per_federation_routing_fees, }: SetConfigurationPayload, - ) -> Result<()> { + ) -> AdminResult<()> { let gw_state = self.get_state().await; let lightning_network = match gw_state { GatewayState::Running { lightning_context } => { if network.is_some() && network != Some(lightning_context.lightning_network) { - return Err(GatewayError::GatewayConfigurationError( + return Err(AdminGatewayError::GatewayConfigurationError( "Cannot change network while connected to a lightning node".to_string(), )); } @@ -1243,7 +1266,7 @@ impl Gateway { if let Some(network) = network { if !self.federation_manager.read().await.is_empty() { - return Err(GatewayError::GatewayConfigurationError( + return Err(AdminGatewayError::GatewayConfigurationError( "Cannot change network while connected to a federation".to_string(), )); } @@ -1263,7 +1286,7 @@ impl Gateway { prev_config } else { - let password = password.ok_or(GatewayError::GatewayConfigurationError( + let password = password.ok_or(AdminGatewayError::GatewayConfigurationError( "The password field is required when initially configuring the gateway".to_string(), ))?; let password_salt: [u8; 16] = rand::thread_rng().gen(); @@ -1317,12 +1340,16 @@ impl Gateway { } /// Generates an onchain address to fund the gateway's lightning node. - pub async fn handle_get_ln_onchain_address_msg(&self) -> Result
{ + pub async fn handle_get_ln_onchain_address_msg(&self) -> AdminResult
{ let context = self.get_lightning_context().await?; let response = context.lnrpc.get_ln_onchain_address().await?; Address::from_str(&response.address) .map(Address::assume_checked) - .map_err(|e| GatewayError::LightningResponseParseError(e.into())) + .map_err(|e| { + AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata { + failure_reason: e.to_string(), + }) + }) } /// Instructs the Gateway's Lightning node to open a channel to a peer @@ -1335,16 +1362,16 @@ impl Gateway { channel_size_sats, push_amount_sats, }: OpenChannelPayload, - ) -> Result { + ) -> AdminResult { let context = self.get_lightning_context().await?; let res = context .lnrpc .open_channel(pubkey, host, channel_size_sats, push_amount_sats) .await?; Txid::from_str(&res.funding_txid).map_err(|e| { - GatewayError::InvalidMetadata(format!( - "Received invalid channel funding txid string: {e}" - )) + AdminGatewayError::Lightning(LightningRpcError::InvalidMetadata { + failure_reason: format!("Received invalid channel funding txid string {e}"), + }) }) } @@ -1353,7 +1380,7 @@ impl Gateway { pub async fn handle_close_channels_with_peer_msg( &self, CloseChannelsWithPeerPayload { pubkey }: CloseChannelsWithPeerPayload, - ) -> Result { + ) -> AdminResult { let context = self.get_lightning_context().await?; let response = context.lnrpc.close_channels_with_peer(pubkey).await?; Ok(response) @@ -1361,7 +1388,9 @@ impl Gateway { /// Returns a list of Lightning network channels from the Gateway's /// Lightning node. - pub async fn handle_list_active_channels_msg(&self) -> Result> { + pub async fn handle_list_active_channels_msg( + &self, + ) -> AdminResult> { let context = self.get_lightning_context().await?; let channels = context.lnrpc.list_active_channels().await?; Ok(channels) @@ -1369,7 +1398,7 @@ impl Gateway { /// Returns the ecash, lightning, and onchain balances for the gateway and /// the gateway's lightning node. - pub async fn handle_get_balances_msg(&self) -> Result { + pub async fn handle_get_balances_msg(&self) -> AdminResult { let dbtx = self.gateway_db.begin_transaction_nc().await; let federation_infos = self .federation_manager @@ -1398,7 +1427,7 @@ impl Gateway { }) } - pub async fn handle_sync_to_chain_msg(&self, payload: SyncToChainPayload) -> Result<()> { + pub async fn handle_sync_to_chain_msg(&self, payload: SyncToChainPayload) -> AdminResult<()> { self.get_lightning_context() .await? .lnrpc @@ -1412,7 +1441,7 @@ impl Gateway { pub async fn handle_spend_ecash_msg( &self, payload: SpendEcashPayload, - ) -> Result { + ) -> AdminResult { let client = self .select_client(payload.federation_id) .await? @@ -1471,10 +1500,22 @@ impl Gateway { .read() .await .get_client_for_federation_id_prefix(payload.notes.federation_id_prefix()) - .ok_or(anyhow!("Client not found"))?; - let mint = client.value().get_first_module::()?; + .ok_or(FederationNotConnected { + federation_id_prefix: payload.notes.federation_id_prefix(), + })?; + let mint = client + .value() + .get_first_module::() + .map_err(|e| PublicGatewayError::ReceiveEcashError { + failure_reason: format!("Mint module does not exist: {e:?}"), + })?; - let operation_id = mint.reissue_external_notes(payload.notes, ()).await?; + let operation_id = mint + .reissue_external_notes(payload.notes, ()) + .await + .map_err(|e| PublicGatewayError::ReceiveEcashError { + failure_reason: e.to_string(), + })?; if payload.wait { let mut updates = mint .subscribe_reissue_external_notes(operation_id) @@ -1484,7 +1525,9 @@ impl Gateway { while let Some(update) = updates.next().await { if let fedimint_mint_client::ReissueExternalNotesState::Failed(e) = update { - return Err(GatewayError::UnexpectedState(e)); + return Err(PublicGatewayError::ReceiveEcashError { + failure_reason: e.to_string(), + }); } } } @@ -1492,7 +1535,7 @@ impl Gateway { Ok(ReceiveEcashResponse { amount }) } - pub async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> Result<()> { + pub async fn handle_shutdown_msg(&self, task_group: TaskGroup) -> AdminResult<()> { if let GatewayState::Running { lightning_context } = self.get_state().await { self.set_gateway_state(GatewayState::ShuttingDown { lightning_context }) .await; @@ -1517,7 +1560,7 @@ impl Gateway { &self, gateway_config: &GatewayConfiguration, federations: &[(FederationId, FederationConfig)], - ) -> Result<()> { + ) -> AdminResult<()> { if let Ok(lightning_context) = self.get_lightning_context().await { let route_hints = lightning_context .lnrpc @@ -1529,7 +1572,7 @@ impl Gateway { for (federation_id, federation_config) in federations { if let Some(client) = self.federation_manager.read().await.client(federation_id) { - if let Err(e) = async { + if async { client .value() .get_first_module::()? @@ -1543,12 +1586,11 @@ impl Gateway { } .instrument(client.span()) .await + .is_err() { - Err(GatewayError::FederationError(FederationError::general( - REGISTER_GATEWAY_ENDPOINT, - serde_json::Value::Null, - anyhow::anyhow!("Error registering federation {federation_id}: {e:?}"), - )))?; + Err(AdminGatewayError::RegistrationError { + federation_id: *federation_id, + })?; } } } @@ -1603,34 +1645,35 @@ impl Gateway { pub async fn select_client( &self, federation_id: FederationId, - ) -> Result> { + ) -> std::result::Result, FederationNotConnected> + { self.federation_manager .read() .await .client(&federation_id) .cloned() - .ok_or(GatewayError::InvalidMetadata(format!( - "No federation with id {federation_id}" - ))) + .ok_or(FederationNotConnected { + federation_id_prefix: federation_id.to_prefix(), + }) } /// Loads a mnemonic from the database or generates a new one if the /// mnemonic does not exist. Before generating a new mnemonic, this /// function will check if a mnemonic has been provided in the environment /// variable and use that if provided. - async fn load_or_generate_mnemonic(gateway_db: &Database) -> Result { + async fn load_or_generate_mnemonic(gateway_db: &Database) -> AdminResult { Ok( if let Ok(entropy) = Client::load_decodable_client_secret::>(gateway_db).await { Mnemonic::from_entropy(&entropy) - .map_err(|e| GatewayError::ClientCreationError(e.to_string()))? + .map_err(|e| AdminGatewayError::MnemonicError(anyhow!(e.to_string())))? } else { let mnemonic = if let Ok(words) = std::env::var(FM_GATEWAY_MNEMONIC_ENV) { info!("Using provided mnemonic from environment variable"); Mnemonic::parse_in_normalized(bip39::Language::English, words.as_str()) .map_err(|e| { - GatewayError::InvalidMetadata(format!( - "Seed phrase provided in environment variable was invalid: {e:?}" - )) + AdminGatewayError::MnemonicError(anyhow!(format!( + "Seed phrase provided in environment was invalid {e:?}" + ))) })? } else { info!("Generating mnemonic and writing entropy to client storage"); @@ -1639,7 +1682,7 @@ impl Gateway { Client::store_encodable_client_secret(gateway_db, mnemonic.to_entropy()) .await - .map_err(|e| GatewayError::ClientCreationError(e.to_string()))?; + .map_err(AdminGatewayError::MnemonicError)?; mnemonic }, ) @@ -1648,7 +1691,7 @@ impl Gateway { /// Reads the connected federation client configs from the Gateway's /// database and reconstructs the clients necessary for interacting with /// connection federations. - async fn load_clients(&self) -> Result<()> { + async fn load_clients(&self) -> AdminResult<()> { let mut federation_manager = self.federation_manager.write().await; let configs = { @@ -1689,7 +1732,7 @@ impl Gateway { let gateway = self.clone(); task_group.spawn_cancellable("register clients", async move { loop { - let mut registration_result: Option> = None; + let mut registration_result: Option> = None; let gateway_config = gateway.clone_gateway_config().await; if let Some(gateway_config) = gateway_config { let gateway_state = gateway.get_state().await; @@ -1709,7 +1752,7 @@ impl Gateway { warn!("Cannot register clients because gateway configuration is not set."); } - let registration_delay: Duration = if let Some(Err(GatewayError::FederationError(_))) = registration_result { + let registration_delay: Duration = if let Some(Err(AdminGatewayError::RegistrationError { .. })) = registration_result { // Retry to register gateway with federations in 10 seconds since it failed Duration::from_secs(10) } else { @@ -1725,18 +1768,19 @@ impl Gateway { /// Verifies that the supplied `network` matches the Bitcoin network in the /// connected client's configuration. - async fn check_federation_network(client: &ClientHandleArc, network: Network) -> Result<()> { + async fn check_federation_network( + client: &ClientHandleArc, + network: Network, + ) -> AdminResult<()> { let federation_id = client.federation_id(); let config = client.config().await; let cfg = config .modules .values() .find(|m| LightningCommonInit::KIND == m.kind) - .ok_or_else(|| { - GatewayError::InvalidMetadata(format!( - "Federation {federation_id} does not have a lightning module", - )) - })?; + .ok_or(AdminGatewayError::ClientCreationError(anyhow!(format!( + "Federation {federation_id} does not have a lightning module" + ))))?; let ln_cfg: &LightningClientConfig = cfg.cast()?; if ln_cfg.network != network { @@ -1744,7 +1788,10 @@ impl Gateway { "Federation {federation_id} runs on {} but this gateway supports {network}", ln_cfg.network, ); - return Err(GatewayError::UnsupportedNetwork(ln_cfg.network)); + return Err(AdminGatewayError::ClientCreationError(anyhow!(format!( + "Unsupported network {}", + ln_cfg.network + )))); } Ok(()) @@ -1753,13 +1800,13 @@ impl Gateway { /// Checks the Gateway's current state and returns the proper /// `LightningContext` if it is available. Sometimes the lightning node /// will not be connected and this will return an error. - pub async fn get_lightning_context(&self) -> Result { + pub async fn get_lightning_context( + &self, + ) -> std::result::Result { match self.get_state().await { GatewayState::Running { lightning_context } | GatewayState::ShuttingDown { lightning_context } => Ok(lightning_context), - _ => Err(GatewayError::LightningRpcError( - LightningRpcError::FailedToConnect, - )), + _ => Err(LightningRpcError::FailedToConnect), } } @@ -1837,7 +1884,8 @@ impl Gateway { .expect("Must have client module") .send_payment(payload) .await - .map_err(GatewayError::LNv2OutgoingError) + .map_err(LNv2Error::OutgoingPayment) + .map_err(PublicGatewayError::LNv2) } /// For the LNv2 protocol, this will create an invoice by fetching it from @@ -1849,20 +1897,22 @@ impl Gateway { payload: CreateBolt11InvoicePayload, ) -> Result { if !payload.contract.verify() { - return Err(GatewayError::IncomingContractError( + return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment( "The contract is invalid".to_string(), - )); + ))); } - let payment_info = self - .routing_info_v2(&payload.federation_id) - .await? - .ok_or(anyhow!("Unknown federation"))?; + let payment_info = self.routing_info_v2(&payload.federation_id).await?.ok_or( + LNv2Error::IncomingPayment(format!( + "Federation {} does not exist", + payload.federation_id + )), + )?; if payload.contract.commitment.refund_pk != payment_info.module_public_key { - return Err(GatewayError::IncomingContractError( + return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment( "The incoming contract is keyed to another gateway".to_string(), - )); + ))); } let contract_amount = payment_info @@ -1870,29 +1920,29 @@ impl Gateway { .subtract_fee(payload.invoice_amount.msats); if contract_amount == Amount::ZERO { - return Err(GatewayError::IncomingContractError( + return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment( "Zero amount incoming contracts are not supported".to_string(), - )); + ))); } if contract_amount != payload.contract.commitment.amount { - return Err(GatewayError::IncomingContractError( + return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment( "The contract amount does not pay the correct amount of fees".to_string(), - )); + ))); } if payload.contract.commitment.expiration <= duration_since_epoch().as_secs() { - return Err(GatewayError::IncomingContractError( + return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment( "The contract has already expired".to_string(), - )); + ))); } let payment_hash = match payload.contract.commitment.payment_image { PaymentImage::Hash(payment_hash) => payment_hash, PaymentImage::Point(..) => { - return Err(GatewayError::IncomingContractError( + return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment( "PaymentImage is not a payment hash".to_string(), - )) + ))); } }; @@ -1903,8 +1953,7 @@ impl Gateway { payload.description.clone(), payload.expiry_time, ) - .await - .map_err(|e| anyhow!(e))?; + .await?; let mut dbtx = self.gateway_db.begin_transaction().await; @@ -1917,14 +1966,16 @@ impl Gateway { .await .is_some() { - return Err(GatewayError::IncomingContractError( - "Payment hash is already registered".to_string(), - )); + return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment( + "PaymentHash is already registered".to_string(), + ))); } - dbtx.commit_tx_result() - .await - .map_err(|_| anyhow!("Payment hash is already registered"))?; + dbtx.commit_tx_result().await.map_err(|_| { + PublicGatewayError::LNv2(LNv2Error::IncomingPayment( + "Payment hash is already registered".to_string(), + )) + })?; Ok(invoice) } @@ -1937,7 +1988,7 @@ impl Gateway { amount: Amount, description: Bolt11InvoiceDescription, expiry_time: u32, - ) -> Result { + ) -> std::result::Result { let lnrpc = self.get_lightning_context().await?.lnrpc; let response = match description { @@ -1964,9 +2015,9 @@ impl Gateway { }; Bolt11Invoice::from_str(&response.invoice).map_err(|e| { - GatewayError::LightningRpcError(LightningRpcError::FailedToGetInvoice { + LightningRpcError::FailedToGetInvoice { failure_reason: e.to_string(), - }) + } }) } @@ -1984,13 +2035,15 @@ impl Gateway { .await .load_registered_incoming_contract(payment_image) .await - .ok_or(anyhow!("No corresponding decryption contract available"))?; + .ok_or(PublicGatewayError::LNv2(LNv2Error::IncomingPayment( + "No corresponding decryption contract available".to_string(), + )))?; if registered_incoming_contract.incoming_amount != amount_msats { - return Err(GatewayError::IncomingContractError( + return Err(PublicGatewayError::LNv2(LNv2Error::IncomingPayment( "The available decryption contract's amount is not equal to the requested amount" .to_string(), - )); + ))); } let client = self @@ -2001,70 +2054,3 @@ impl Gateway { Ok((registered_incoming_contract.contract, client)) } } - -/// Errors that can occur while processing incoming HTLC's, making outgoing -/// payments, registering with connected federations, or responding to webserver -/// requests. -#[derive(Debug, Error)] -pub enum GatewayError { - #[error("Federation error: {}", OptStacktrace(.0))] - FederationError(#[from] FederationError), - #[error("Other: {}", OptStacktrace(.0))] - ClientStateMachineError(#[from] anyhow::Error), - #[error("Failed to open the database: {}", OptStacktrace(.0))] - DatabaseError(anyhow::Error), - #[error("Lightning rpc error: {}", .0)] - LightningRpcError(#[from] LightningRpcError), - #[error("Outgoing Payment Error {}", OptStacktrace(.0))] - OutgoingPaymentError(#[from] Box), - #[error("Invalid Metadata: {}", OptStacktrace(.0))] - InvalidMetadata(String), - #[error("Unexpected state: {}", OptStacktrace(.0))] - UnexpectedState(String), - #[error("The gateway is disconnected")] - Disconnected, - #[error("Error configuring the gateway: {}", OptStacktrace(.0))] - GatewayConfigurationError(String), - #[error("Unsupported Network: {0}")] - UnsupportedNetwork(Network), - #[error("Insufficient funds")] - InsufficientFunds, - #[error("Federation already connected")] - FederationAlreadyConnected, - #[error("Error parsing response: {}", OptStacktrace(.0))] - LightningResponseParseError(anyhow::Error), - #[error("An incoming payment was unable to be handled by the LNv1 module")] - IncomingLNv1PaymentError(anyhow::Error), - #[error("Failed to create client: {}", .0)] - ClientCreationError(String), - #[error("Incoming contract error: {}", OptStacktrace(.0))] - IncomingContractError(String), - #[error("Error while sending LNv2 payment: {}", OptStacktrace(.0))] - LNv2OutgoingError(anyhow::Error), -} - -impl IntoResponse for GatewayError { - fn into_response(self) -> Response { - // For privacy reasons, we do not return too many details about the failure of - // the request back to the client to prevent malicious clients from - // deducing state about the gateway/lightning node. - let (error_message, status_code) = match self { - GatewayError::OutgoingPaymentError(_) => ( - "Error while paying lightning invoice. Outgoing contract will be refunded." - .to_string(), - StatusCode::BAD_REQUEST, - ), - GatewayError::Disconnected => ( - "The gateway is disconnected from the Lightning Node".to_string(), - StatusCode::NOT_FOUND, - ), - _ => ( - "An internal gateway error occurred".to_string(), - StatusCode::INTERNAL_SERVER_ERROR, - ), - }; - let mut err = Cow::<'static, str>::Owned(error_message).into_response(); - *err.status_mut() = status_code; - err - } -} diff --git a/gateway/ln-gateway/src/lightning/mod.rs b/gateway/ln-gateway/src/lightning/mod.rs index 3f2a579880f..bce0a0ebb71 100644 --- a/gateway/ln-gateway/src/lightning/mod.rs +++ b/gateway/ln-gateway/src/lightning/mod.rs @@ -35,7 +35,6 @@ use crate::gateway_lnrpc::{ GetBalancesResponse, GetLnOnchainAddressResponse, GetNodeInfoResponse, GetRouteHintsResponse, InterceptHtlcRequest, InterceptHtlcResponse, OpenChannelResponse, PayInvoiceResponse, }; -use crate::GatewayError; pub const MAX_LIGHTNING_RETRIES: u32 = 10; @@ -76,6 +75,8 @@ pub enum LightningRpcError { FailedToSubscribeToInvoiceUpdates { failure_reason: String }, #[error("Failed to sync to chain: {failure_reason}")] FailedToSyncToChain { failure_reason: String }, + #[error("Invalid metadata: {failure_reason}")] + InvalidMetadata { failure_reason: String }, } /// Represents an active connection to the lightning node. @@ -217,7 +218,9 @@ impl dyn ILnRpcClient { /// Retrieves the basic information about the Gateway's connected Lightning /// node. - pub async fn parsed_node_info(&self) -> super::Result<(PublicKey, String, Network, u32, bool)> { + pub async fn parsed_node_info( + &self, + ) -> std::result::Result<(PublicKey, String, Network, u32, bool), LightningRpcError> { let GetNodeInfoResponse { pub_key, alias, @@ -225,11 +228,14 @@ impl dyn ILnRpcClient { block_height, synced_to_chain, } = self.info().await?; - let node_pub_key = PublicKey::from_slice(&pub_key) - .map_err(|e| GatewayError::InvalidMetadata(format!("Invalid node pubkey {e}")))?; - let network = Network::from_str(&network).map_err(|e| { - GatewayError::InvalidMetadata(format!("Invalid network {network}: {e}")) - })?; + let node_pub_key = + PublicKey::from_slice(&pub_key).map_err(|e| LightningRpcError::InvalidMetadata { + failure_reason: format!("Invalid node pubkey {e}"), + })?; + let network = + Network::from_str(&network).map_err(|e| LightningRpcError::InvalidMetadata { + failure_reason: format!("Invalid network {network}: {e}"), + })?; Ok((node_pub_key, alias, network, block_height, synced_to_chain)) } } diff --git a/gateway/ln-gateway/src/rpc/rpc_server.rs b/gateway/ln-gateway/src/rpc/rpc_server.rs index afee6fb3f54..157cd6e20ef 100644 --- a/gateway/ln-gateway/src/rpc/rpc_server.rs +++ b/gateway/ln-gateway/src/rpc/rpc_server.rs @@ -37,8 +37,9 @@ use super::{ SetConfigurationPayload, SpendEcashPayload, SyncToChainPayload, WithdrawPayload, V1_API_ENDPOINT, }; +use crate::error::{AdminGatewayError, PublicGatewayError}; use crate::rpc::ConfigPayload; -use crate::{Gateway, GatewayError}; +use crate::Gateway; /// Creates the webserver's routes and spawns the webserver in a separate task. pub async fn run_webserver(gateway: Arc, task_group: TaskGroup) -> anyhow::Result<()> { @@ -231,7 +232,7 @@ pub fn hash_password(plaintext_password: &str, salt: [u8; 16]) -> sha256::Hash { async fn handle_post_info( Extension(gateway): Extension>, Json(_payload): Json, -) -> Result { +) -> Result { let info = gateway.handle_get_info().await?; Ok(Json(json!(info))) } @@ -240,7 +241,7 @@ async fn handle_post_info( #[instrument(skip_all, err)] async fn info( Extension(gateway): Extension>, -) -> Result { +) -> Result { let info = gateway.handle_get_info().await?; Ok(Json(json!(info))) } @@ -250,7 +251,7 @@ async fn info( async fn configuration( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let gateway_fed_config = gateway .handle_get_federation_config(payload.federation_id) .await?; @@ -262,7 +263,7 @@ async fn configuration( async fn balance( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let amount = gateway.handle_balance_msg(payload).await?; Ok(Json(json!(amount))) } @@ -272,7 +273,7 @@ async fn balance( async fn address( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let address = gateway.handle_address_msg(payload).await?; Ok(Json(json!(address))) } @@ -282,7 +283,7 @@ async fn address( async fn withdraw( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let txid = gateway.handle_withdraw_msg(payload).await?; Ok(Json(json!(txid))) } @@ -291,7 +292,7 @@ async fn withdraw( async fn create_invoice_for_self( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let invoice = gateway.handle_create_invoice_for_self_msg(payload).await?; Ok(Json(json!(invoice))) } @@ -300,7 +301,7 @@ async fn create_invoice_for_self( async fn pay_invoice_self( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let preimage = gateway.handle_pay_invoice_self_msg(payload).await?; Ok(Json(json!(preimage.0.encode_hex::()))) } @@ -309,7 +310,7 @@ async fn pay_invoice_self( async fn pay_invoice( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let preimage = gateway.handle_pay_invoice_msg(payload).await?; Ok(Json(json!(preimage.0.encode_hex::()))) } @@ -319,7 +320,7 @@ async fn pay_invoice( async fn connect_fed( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let fed = gateway.handle_connect_federation(payload).await?; Ok(Json(json!(fed))) } @@ -329,7 +330,7 @@ async fn connect_fed( async fn leave_fed( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let fed = gateway.handle_leave_federation(payload).await?; Ok(Json(json!(fed))) } @@ -339,7 +340,7 @@ async fn leave_fed( async fn backup( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { gateway.handle_backup_msg(payload).await?; Ok(Json(json!(()))) } @@ -348,7 +349,7 @@ async fn backup( async fn set_configuration( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { gateway.handle_set_configuration_msg(payload).await?; Ok(Json(json!(()))) } @@ -357,7 +358,7 @@ async fn set_configuration( async fn get_ln_onchain_address( Extension(gateway): Extension>, Json(_payload): Json, -) -> Result { +) -> Result { let address = gateway.handle_get_ln_onchain_address_msg().await?; Ok(Json(json!(address.to_string()))) } @@ -366,7 +367,7 @@ async fn get_ln_onchain_address( async fn open_channel( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let funding_txid = gateway.handle_open_channel_msg(payload).await?; Ok(Json(json!(funding_txid))) } @@ -375,7 +376,7 @@ async fn open_channel( async fn close_channels_with_peer( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let response = gateway.handle_close_channels_with_peer_msg(payload).await?; Ok(Json(json!(response))) } @@ -383,7 +384,7 @@ async fn close_channels_with_peer( #[instrument(skip_all, err)] async fn list_active_channels( Extension(gateway): Extension>, -) -> Result { +) -> Result { let channels = gateway.handle_list_active_channels_msg().await?; Ok(Json(json!(channels))) } @@ -391,7 +392,7 @@ async fn list_active_channels( #[instrument(skip_all, err)] async fn get_balances( Extension(gateway): Extension>, -) -> Result { +) -> Result { let balances = gateway.handle_get_balances_msg().await?; Ok(Json(json!(balances))) } @@ -400,7 +401,7 @@ async fn get_balances( async fn sync_to_chain( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { gateway.handle_sync_to_chain_msg(payload).await?; Ok(Json(json!(()))) } @@ -408,7 +409,7 @@ async fn sync_to_chain( #[instrument(skip_all, err)] async fn get_gateway_id( Extension(gateway): Extension>, -) -> Result { +) -> Result { Ok(Json(json!(gateway.gateway_id))) } @@ -416,7 +417,7 @@ async fn get_gateway_id( async fn routing_info_v2( Extension(gateway): Extension>, Json(federation_id): Json, -) -> Result { +) -> Result { let routing_info = gateway.routing_info_v2(&federation_id).await?; Ok(Json(json!(routing_info))) } @@ -425,7 +426,7 @@ async fn routing_info_v2( async fn pay_bolt11_invoice_v2( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let payment_result = gateway.send_payment_v2(payload).await?; Ok(Json(json!(payment_result))) } @@ -434,7 +435,7 @@ async fn pay_bolt11_invoice_v2( async fn create_bolt11_invoice_v2( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { let invoice = gateway.create_bolt11_invoice_v2(payload).await?; Ok(Json(json!(invoice))) } @@ -443,7 +444,7 @@ async fn create_bolt11_invoice_v2( async fn spend_ecash( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { Ok(Json(json!(gateway.handle_spend_ecash_msg(payload).await?))) } @@ -451,7 +452,7 @@ async fn spend_ecash( async fn receive_ecash( Extension(gateway): Extension>, Json(payload): Json, -) -> Result { +) -> Result { Ok(Json(json!( gateway.handle_receive_ecash_msg(payload).await? ))) @@ -460,7 +461,7 @@ async fn receive_ecash( #[instrument(skip_all, err)] async fn mnemonic( Extension(gateway): Extension>, -) -> Result { +) -> Result { let words = gateway.handle_mnemonic_msg().await?; Ok(Json(json!(words))) } @@ -469,7 +470,7 @@ async fn mnemonic( async fn stop( Extension(task_group): Extension, Extension(gateway): Extension>, -) -> Result { +) -> Result { gateway.handle_shutdown_msg(task_group).await?; Ok(Json(json!(()))) }