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