fedimint_gateway_server/
error.rs

1use std::fmt::Display;
2
3use axum::Json;
4use axum::body::Body;
5use axum::response::{IntoResponse, Response};
6use fedimint_core::config::{FederationId, FederationIdPrefix};
7use fedimint_core::crit;
8use fedimint_core::envs::is_env_var_set;
9use fedimint_core::fmt_utils::OptStacktrace;
10use fedimint_core::util::FmtCompactAnyhow;
11use fedimint_gw_client::pay::OutgoingPaymentError;
12use fedimint_lightning::LightningRpcError;
13use fedimint_logging::LOG_GATEWAY;
14use reqwest::StatusCode;
15use thiserror::Error;
16
17use crate::envs::FM_DEBUG_GATEWAY_ENV;
18
19/// Top level error enum for all errors that can occur in the Gateway.
20#[derive(Debug, thiserror::Error)]
21pub enum GatewayError {
22    #[error("Admin error: {0}")]
23    Admin(#[from] AdminGatewayError),
24    #[error("Public error: {0}")]
25    Public(#[from] PublicGatewayError),
26    #[error("{0}")]
27    Lnurl(#[from] LnurlError),
28}
29
30impl IntoResponse for GatewayError {
31    fn into_response(self) -> Response {
32        match self {
33            GatewayError::Admin(admin) => admin.into_response(),
34            GatewayError::Public(public) => public.into_response(),
35            GatewayError::Lnurl(lnurl) => lnurl.into_response(),
36        }
37    }
38}
39
40/// Errors that unauthenticated endpoints can encounter. For privacy reasons,
41/// the error messages are intended to be redacted before returning to the
42/// client.
43#[derive(Debug, Error)]
44pub enum PublicGatewayError {
45    #[error("Lightning rpc error: {}", .0)]
46    Lightning(#[from] LightningRpcError),
47    #[error("LNv1 error: {:?}", .0)]
48    LNv1(#[from] LNv1Error),
49    #[error("LNv2 error: {:?}", .0)]
50    LNv2(#[from] LNv2Error),
51    #[error("{}", .0)]
52    FederationNotConnected(#[from] FederationNotConnected),
53    #[error("Failed to receive ecash: {failure_reason}")]
54    ReceiveEcashError { failure_reason: String },
55    #[error("Unexpected Error: {}", OptStacktrace(.0))]
56    Unexpected(#[from] anyhow::Error),
57}
58
59impl IntoResponse for PublicGatewayError {
60    fn into_response(self) -> Response {
61        // For privacy reasons, we do not return too many details about the failure of
62        // the request back to the client to prevent malicious clients from
63        // deducing state about the gateway/lightning node.
64        crit!(target: LOG_GATEWAY, "{self}");
65        let (error_message, status_code) = match &self {
66            PublicGatewayError::FederationNotConnected(e) => {
67                (e.to_string(), StatusCode::BAD_REQUEST)
68            }
69            PublicGatewayError::ReceiveEcashError { .. } => (
70                "Failed to receive ecash".to_string(),
71                StatusCode::INTERNAL_SERVER_ERROR,
72            ),
73            PublicGatewayError::Lightning(_) => (
74                "Lightning Network operation failed".to_string(),
75                StatusCode::INTERNAL_SERVER_ERROR,
76            ),
77            PublicGatewayError::LNv1(_) => (
78                "LNv1 operation failed, please contact gateway operator".to_string(),
79                StatusCode::INTERNAL_SERVER_ERROR,
80            ),
81            PublicGatewayError::LNv2(_) => (
82                "LNv2 operation failed, please contact gateway operator".to_string(),
83                StatusCode::INTERNAL_SERVER_ERROR,
84            ),
85            PublicGatewayError::Unexpected(e) => (e.to_string(), StatusCode::BAD_REQUEST),
86        };
87
88        let error_message = if is_env_var_set(FM_DEBUG_GATEWAY_ENV) {
89            self.to_string()
90        } else {
91            error_message
92        };
93
94        Response::builder()
95            .status(status_code)
96            .body(error_message.into())
97            .expect("Failed to create Response")
98    }
99}
100
101/// Errors that authenticated endpoints can encounter. Full error message and
102/// error details are returned to the admin client for debugging purposes.
103#[derive(Debug, Error)]
104pub enum AdminGatewayError {
105    #[error("Failed to create a federation client")]
106    ClientCreationError(anyhow::Error),
107    #[error("Failed to remove a federation client: {0}")]
108    ClientRemovalError(String),
109    #[error("There was an error with the Gateway's mnemonic: {}", .0.fmt_compact_anyhow())]
110    MnemonicError(anyhow::Error),
111    #[error("Unexpected Error: {}", .0.fmt_compact_anyhow())]
112    Unexpected(#[from] anyhow::Error),
113    #[error("{}", .0)]
114    FederationNotConnected(#[from] FederationNotConnected),
115    #[error("Error configuring the gateway: {0}")]
116    GatewayConfigurationError(String),
117    #[error("Lightning error: {}", .0)]
118    Lightning(#[from] LightningRpcError),
119    #[error("Error registering federation {federation_id}")]
120    RegistrationError { federation_id: FederationId },
121    #[error("Error withdrawing funds onchain: {failure_reason}")]
122    WithdrawError { failure_reason: String },
123}
124
125impl IntoResponse for AdminGatewayError {
126    // For admin errors, always pass along the full error message for debugging
127    // purposes
128    fn into_response(self) -> Response {
129        crit!(target: LOG_GATEWAY, "{self}");
130        Response::builder()
131            .status(StatusCode::INTERNAL_SERVER_ERROR)
132            .body(self.to_string().into())
133            .expect("Failed to create Response")
134    }
135}
136
137/// Errors that can occur during the LNv1 protocol. LNv1 errors are public and
138/// the error messages should be redacted for privacy reasons.
139#[derive(Debug, Error)]
140pub enum LNv1Error {
141    #[error("Incoming payment error: {}", OptStacktrace(.0))]
142    IncomingPayment(String),
143    #[error(
144        "Outgoing Contract Error Reason: {message} Stack: {}",
145        OptStacktrace(error)
146    )]
147    OutgoingContract {
148        error: Box<OutgoingPaymentError>,
149        message: String,
150    },
151    #[error("Outgoing Payment Error: {}", OptStacktrace(.0))]
152    OutgoingPayment(#[from] anyhow::Error),
153}
154
155/// Errors that can occur during the LNv2 protocol. LNv2 errors are public and
156/// the error messages should be redacted for privacy reasons.
157#[derive(Debug, Error)]
158pub enum LNv2Error {
159    #[error("Incoming Payment Error: {}", .0)]
160    IncomingPayment(String),
161    #[error("Outgoing Payment Error: {}", OptStacktrace(.0))]
162    OutgoingPayment(#[from] anyhow::Error),
163}
164
165/// Public error that indicates the requested federation is not connected to
166/// this gateway.
167#[derive(Debug, Error)]
168pub struct FederationNotConnected {
169    pub federation_id_prefix: FederationIdPrefix,
170}
171
172impl Display for FederationNotConnected {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        write!(
175            f,
176            "No federation available for prefix {}",
177            self.federation_id_prefix
178        )
179    }
180}
181
182/// LNURL-compliant error response for verify endpoints
183#[derive(Debug, Error)]
184pub(crate) struct LnurlError {
185    code: StatusCode,
186    reason: anyhow::Error,
187}
188
189impl Display for LnurlError {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        write!(f, "LNURL Error: {}", self.reason,)
192    }
193}
194
195impl LnurlError {
196    pub(crate) fn internal(reason: anyhow::Error) -> Self {
197        Self {
198            code: StatusCode::INTERNAL_SERVER_ERROR,
199            reason,
200        }
201    }
202}
203
204impl IntoResponse for LnurlError {
205    fn into_response(self) -> Response<Body> {
206        let json = Json(serde_json::json!({
207            "status": "ERROR",
208            "reason": self.reason.to_string(),
209        }));
210
211        (self.code, json).into_response()
212    }
213}