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::FmtCompact;
11use fedimint_gw_client::pay::OutgoingPaymentError;
12use fedimint_lightning::LightningRpcError;
13use fedimint_logging::LOG_GATEWAY;
14use reqwest::StatusCode;
15use thiserror::Error;
16use tracing::debug;
17
18use crate::envs::FM_DEBUG_GATEWAY_ENV;
19
20#[derive(Debug, thiserror::Error)]
22pub enum GatewayError {
23 #[error("Admin error: {0}")]
24 Admin(#[from] AdminGatewayError),
25 #[error("Public error: {0}")]
26 Public(#[from] PublicGatewayError),
27 #[error("{0}")]
28 Lnurl(#[from] LnurlError),
29}
30
31impl IntoResponse for GatewayError {
32 fn into_response(self) -> Response {
33 match self {
34 GatewayError::Admin(admin) => admin.into_response(),
35 GatewayError::Public(public) => public.into_response(),
36 GatewayError::Lnurl(lnurl) => lnurl.into_response(),
37 }
38 }
39}
40
41#[derive(Debug, Error)]
45pub enum PublicGatewayError {
46 #[error("Gateway invariant violation: {}", OptStacktrace(.0))]
47 Internal(anyhow::Error),
48 #[error("Lightning rpc error: {}", .0)]
49 Lightning(#[from] LightningRpcError),
50 #[error("LNv1 error: {:?}", .0)]
51 LNv1(#[from] LNv1Error),
52 #[error("LNv2 error: {:?}", .0)]
53 LNv2(#[from] LNv2Error),
54 #[error("{}", .0)]
55 FederationNotConnected(#[from] FederationNotConnected),
56 #[error("Failed to receive ecash: {failure_reason}")]
57 ReceiveEcashError { failure_reason: String },
58 #[error("Too many requests")]
59 RateLimited,
60 #[error("Receiving payments is disabled for federation {federation_id_prefix}")]
61 ReceiveDisabled {
62 federation_id_prefix: FederationIdPrefix,
63 },
64 #[error("Unexpected Error: {}", OptStacktrace(.0))]
65 Unexpected(#[from] anyhow::Error),
66}
67
68impl IntoResponse for PublicGatewayError {
69 fn into_response(self) -> Response {
70 if matches!(self, PublicGatewayError::RateLimited) {
78 debug!(target: LOG_GATEWAY, "{self}");
79 } else {
80 crit!(target: LOG_GATEWAY, "{self}");
81 }
82 let (error_message, status_code) = match &self {
83 PublicGatewayError::Internal(_) => (
84 "Gateway internal error".to_owned(),
85 StatusCode::INTERNAL_SERVER_ERROR,
86 ),
87 PublicGatewayError::FederationNotConnected(e) => {
88 (e.to_string(), StatusCode::BAD_REQUEST)
89 }
90 PublicGatewayError::ReceiveEcashError { .. } => (
91 "Failed to receive ecash".to_string(),
92 StatusCode::INTERNAL_SERVER_ERROR,
93 ),
94 PublicGatewayError::Lightning(_) => (
95 "Lightning Network operation failed".to_string(),
96 StatusCode::INTERNAL_SERVER_ERROR,
97 ),
98 PublicGatewayError::LNv1(_) => (
99 "LNv1 operation failed, please contact gateway operator".to_string(),
100 StatusCode::INTERNAL_SERVER_ERROR,
101 ),
102 PublicGatewayError::LNv2(_) => (
103 "LNv2 operation failed, please contact gateway operator".to_string(),
104 StatusCode::INTERNAL_SERVER_ERROR,
105 ),
106 PublicGatewayError::RateLimited => (
107 "Too many requests, please try again later".to_string(),
108 StatusCode::TOO_MANY_REQUESTS,
109 ),
110 PublicGatewayError::ReceiveDisabled { .. } => {
113 (self.to_string(), StatusCode::BAD_REQUEST)
114 }
115 PublicGatewayError::Unexpected(e) => (e.to_string(), StatusCode::BAD_REQUEST),
116 };
117
118 let error_message =
119 self.response_message(error_message, is_env_var_set(FM_DEBUG_GATEWAY_ENV));
120
121 Response::builder()
122 .status(status_code)
123 .body(error_message.into())
124 .expect("Failed to create Response")
125 }
126}
127
128impl PublicGatewayError {
129 fn response_message(&self, sanitized: String, expose_debug_details: bool) -> String {
130 if expose_debug_details && !matches!(self, PublicGatewayError::Internal(_)) {
131 self.to_string()
132 } else {
133 sanitized
134 }
135 }
136}
137
138#[cfg(test)]
139#[path = "error/tests.rs"]
140mod tests;
141
142#[derive(Debug, Error)]
145pub enum AdminGatewayError {
146 #[error("Failed to create a federation client")]
147 ClientCreationError(anyhow::Error),
148 #[error("Failed to remove a federation client: {0}")]
149 ClientRemovalError(String),
150 #[error("There was an error with the Gateway's mnemonic: {}", .0.fmt_compact())]
151 MnemonicError(anyhow::Error),
152 #[error("Unexpected Error: {}", .0.fmt_compact())]
153 Unexpected(#[from] anyhow::Error),
154 #[error("{}", .0)]
155 FederationNotConnected(#[from] FederationNotConnected),
156 #[error("Error configuring the gateway: {0}")]
157 GatewayConfigurationError(String),
158 #[error("Lightning error: {}", .0)]
159 Lightning(#[from] LightningRpcError),
160 #[error("Error registering federation {federation_id}")]
161 RegistrationError { federation_id: FederationId },
162 #[error("Error withdrawing funds onchain: {failure_reason}")]
163 WithdrawError { failure_reason: String },
164}
165
166impl IntoResponse for AdminGatewayError {
167 fn into_response(self) -> Response {
170 crit!(target: LOG_GATEWAY, "{self}");
171 Response::builder()
172 .status(StatusCode::INTERNAL_SERVER_ERROR)
173 .body(self.to_string().into())
174 .expect("Failed to create Response")
175 }
176}
177
178#[derive(Debug, Error)]
181pub enum LNv1Error {
182 #[error("Incoming payment error: {}", OptStacktrace(.0))]
183 IncomingPayment(String),
184 #[error(
185 "Outgoing Contract Error Reason: {message} Stack: {}",
186 OptStacktrace(error)
187 )]
188 OutgoingContract {
189 error: Box<OutgoingPaymentError>,
190 message: String,
191 },
192 #[error("Outgoing Payment Error: {}", OptStacktrace(.0))]
193 OutgoingPayment(#[from] anyhow::Error),
194}
195
196#[derive(Debug, Error)]
199pub enum LNv2Error {
200 #[error("Incoming Payment Error: {}", .0)]
201 IncomingPayment(String),
202 #[error(
204 "Incoming Payment Error: the registered contract is for {registered_msats} msat, the payment carries {payment_msats} msat"
205 )]
206 IncomingAmountMismatch {
207 registered_msats: u64,
208 payment_msats: u64,
209 },
210 #[error("Outgoing Payment Error: {}", OptStacktrace(.0))]
211 OutgoingPayment(#[from] anyhow::Error),
212}
213
214#[derive(Debug, Error)]
217pub struct FederationNotConnected {
218 pub federation_id_prefix: FederationIdPrefix,
219}
220
221impl Display for FederationNotConnected {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 write!(
224 f,
225 "No federation available for prefix {}",
226 self.federation_id_prefix
227 )
228 }
229}
230
231#[derive(Debug, Error)]
233pub(crate) struct LnurlError {
234 code: StatusCode,
235 reason: anyhow::Error,
236}
237
238impl Display for LnurlError {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 write!(f, "LNURL Error: {}", self.reason,)
241 }
242}
243
244impl LnurlError {
245 pub(crate) fn internal(reason: anyhow::Error) -> Self {
246 Self {
247 code: StatusCode::INTERNAL_SERVER_ERROR,
248 reason,
249 }
250 }
251}
252
253impl IntoResponse for LnurlError {
254 fn into_response(self) -> Response<Body> {
255 let json = Json(serde_json::json!({
256 "status": "ERROR",
257 "reason": self.reason.to_string(),
258 }));
259
260 (self.code, json).into_response()
261 }
262}