mostro_core/error.rs
1//! Error taxonomy used across the crate.
2//!
3//! Errors surfaced to clients are modelled as [`MostroError`], which is split
4//! into two branches:
5//!
6//! * [`MostroError::MostroCantDo`] — a "soft" error: the request was
7//! well-formed but the server refuses to perform the action (e.g. the order
8//! is not in the right state). Clients should surface the inner
9//! [`CantDoReason`] to the user.
10//! * [`MostroError::MostroInternalErr`] — a "hard" error: something went
11//! wrong while processing the request (database failure, Nostr relay
12//! issue, malformed invoice, etc.). The inner [`ServiceError`] carries the
13//! diagnostic detail.
14//!
15//! Both inner enums implement [`Display`](std::fmt::Display) with
16//! human-readable messages suited for logging.
17
18use crate::prelude::*;
19
20/// Machine-readable reasons carried by a `CantDo` response.
21///
22/// Serialized in `snake_case` so clients can pattern-match on the value
23/// transported in [`crate::message::Payload::CantDo`].
24#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
25#[serde(rename_all = "snake_case")]
26pub enum CantDoReason {
27 /// The provided signature is invalid or missing.
28 InvalidSignature,
29 /// The specified trade index does not exist or is invalid.
30 InvalidTradeIndex,
31 /// The provided amount is invalid or out of acceptable range.
32 InvalidAmount,
33 /// The provided invoice is malformed or expired.
34 InvalidInvoice,
35 /// The payment request is invalid or cannot be processed.
36 InvalidPaymentRequest,
37 /// The specified peer is invalid or not found.
38 InvalidPeer,
39 /// The rating value is invalid or out of range.
40 InvalidRating,
41 /// The text message is invalid or contains prohibited content.
42 InvalidTextMessage,
43 /// The order kind is invalid.
44 InvalidOrderKind,
45 /// The order status is invalid.
46 InvalidOrderStatus,
47 /// The provided public key is invalid.
48 InvalidPubkey,
49 /// One or more request parameters are invalid.
50 InvalidParameters,
51 /// The provided payload is the wrong shape for this action,
52 /// or carries values that cannot be processed (e.g. a
53 /// `BondResolution` requesting a slash against a side with no
54 /// active bond row). The caller should correct and resend.
55 InvalidPayload,
56 /// The targeted order has already been canceled.
57 OrderAlreadyCanceled,
58 /// User creation failed on the server side.
59 CantCreateUser,
60 /// The caller tried to operate on an order that does not belong to them.
61 IsNotYourOrder,
62 /// The requested action is not allowed in the order's current status.
63 NotAllowedByStatus,
64 /// The fiat amount is outside the allowed range for this order.
65 OutOfRangeFiatAmount,
66 /// The sats amount is outside the allowed range for this order.
67 OutOfRangeSatsAmount,
68 /// No fresh exchange rate is available for the order's fiat currency:
69 /// the last-known-good price is older than the node's staleness window,
70 /// so a market-priced order cannot be created or taken right now. The
71 /// caller should retry once pricing recovers, or use a fixed rate.
72 PriceTooStale,
73 /// The caller tried to operate on a dispute that does not belong to them.
74 IsNotYourDispute,
75 /// A solver is being notified that an admin has taken over their dispute.
76 DisputeTakenByAdmin,
77 /// The caller is authenticated but lacks the permission for this action.
78 NotAuthorized,
79 /// A dispute could not be created (e.g. order not in a disputable state).
80 DisputeCreationError,
81 /// Generic "resource not found" error.
82 NotFound,
83 /// The dispute is in an invalid state for the requested action.
84 InvalidDisputeStatus,
85 /// The requested action is invalid.
86 InvalidAction,
87 /// The caller already has a pending order and cannot create another.
88 PendingOrderExists,
89 /// The fiat currency code is not accepted by this Mostro node.
90 InvalidFiatCurrency,
91 /// The caller is being rate-limited.
92 TooManyRequests,
93 /// The submitted Cashu token is malformed, cannot be parsed, or its
94 /// 2-of-3 spending condition does not match the expected
95 /// buyer/seller/Mostro pubkeys.
96 InvalidCashuToken,
97 /// The configured Cashu mint could not be reached or did not answer the
98 /// state check.
99 CashuMintUnavailable,
100 /// The provided mint URL is malformed or does not match the node's
101 /// configured mint.
102 InvalidMintUrl,
103 /// The requested action needs a locked Cashu escrow, but none has been
104 /// recorded for this order.
105 CashuEscrowNotLocked,
106 /// A required Cashu signature is missing from the request.
107 CashuSignatureMissing,
108}
109
110/// Internal errors raised by services behind the Mostro API.
111///
112/// Unlike [`CantDoReason`], values of this enum are not expected to be
113/// forwarded verbatim to end users; they are meant for logs, telemetry and
114/// other server-to-server diagnostics.
115#[derive(Debug, PartialEq, Eq)]
116pub enum ServiceError {
117 /// Wraps an error returned by `nostr_sdk`.
118 NostrError(String),
119 /// The invoice string could not be parsed as a valid BOLT-11 invoice.
120 ParsingInvoiceError,
121 /// A numeric value could not be parsed.
122 ParsingNumberError,
123 /// The invoice has expired.
124 InvoiceExpiredError,
125 /// The invoice is otherwise invalid.
126 InvoiceInvalidError,
127 /// The invoice expiration time is below the minimum required.
128 MinExpirationTimeError,
129 /// The invoice amount is below the minimum allowed.
130 MinAmountError,
131 /// The invoice amount does not match the expected value.
132 WrongAmountError,
133 /// The price API did not answer in time.
134 NoAPIResponse,
135 /// The requested currency is not listed by the exchange API.
136 NoCurrency,
137 /// A price exists but is older than the configured staleness window, so
138 /// it must not be used to price an order (the multi-source price
139 /// manager serves last-known-good only up to that TTL).
140 PriceTooStale,
141 /// The exchange API returned a response that could not be parsed.
142 MalformedAPIRes,
143 /// Amount value is negative where only positives are allowed.
144 NegativeAmount,
145 /// A Lightning Address could not be parsed.
146 LnAddressParseError,
147 /// A Lightning Address payment was attempted with a wrong amount.
148 LnAddressWrongAmount,
149 /// A Lightning payment failed; the inner string carries the reason.
150 LnPaymentError(String),
151 /// Communication with the Lightning node failed.
152 LnNodeError(String),
153 /// Order id was not found in the database.
154 InvalidOrderId,
155 /// Database access failed; the inner string carries the detail.
156 DbAccessError(String),
157 /// The provided public key is invalid.
158 InvalidPubkey,
159 /// Hold-invoice operation failed; the inner string carries the detail.
160 HoldInvoiceError(String),
161 /// Could not update the order status in the database.
162 UpdateOrderStatusError,
163 /// The order status is invalid.
164 InvalidOrderStatus,
165 /// The order kind is invalid.
166 InvalidOrderKind,
167 /// A dispute already exists for this order.
168 DisputeAlreadyExists,
169 /// Could not publish the dispute Nostr event.
170 DisputeEventError,
171 /// The rating message itself is invalid.
172 InvalidRating,
173 /// The rating value is outside the accepted range.
174 InvalidRatingValue,
175 /// Failed to serialize or deserialize a [`crate::message::Message`].
176 MessageSerializationError,
177 /// The dispute id is invalid or unknown.
178 InvalidDisputeId,
179 /// The dispute status is invalid.
180 InvalidDisputeStatus,
181 /// The payload does not match the action.
182 InvalidPayload,
183 /// Any other unexpected error; inner string carries the detail.
184 UnexpectedError(String),
185 /// An environment variable could not be read or parsed.
186 EnvVarError(String),
187 /// Underlying I/O error.
188 IOError(String),
189 /// NIP-44/NIP-59 encryption failed.
190 EncryptionError(String),
191 /// NIP-44/NIP-59 decryption failed.
192 DecryptionError(String),
193}
194
195/// Top-level error type returned by the Mostro API surface.
196///
197/// Most public functions in this crate return `Result<T, MostroError>`.
198/// Match on the variants to distinguish between user-actionable "can't do"
199/// responses and internal service errors.
200#[derive(Debug, PartialEq, Eq)]
201pub enum MostroError {
202 /// An internal service-level error; diagnostic only.
203 MostroInternalErr(ServiceError),
204 /// A structured "can't do" response to surface to the user.
205 MostroCantDo(CantDoReason),
206}
207
208impl std::error::Error for MostroError {}
209
210impl std::fmt::Display for MostroError {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 match self {
213 MostroError::MostroInternalErr(m) => write!(f, "Error caused by {}", m),
214 MostroError::MostroCantDo(m) => write!(f, "Sending cantDo message to user for {:?}", m),
215 }
216 }
217}
218
219impl std::fmt::Display for ServiceError {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 match self {
222 ServiceError::ParsingInvoiceError => write!(f, "Incorrect invoice"),
223 ServiceError::ParsingNumberError => write!(f, "Error parsing the number"),
224 ServiceError::InvoiceExpiredError => write!(f, "Invoice has expired"),
225 ServiceError::MinExpirationTimeError => write!(f, "Minimal expiration time on invoice"),
226 ServiceError::InvoiceInvalidError => write!(f, "Invoice is invalid"),
227 ServiceError::MinAmountError => write!(f, "Minimal payment amount"),
228 ServiceError::WrongAmountError => write!(f, "The amount on this invoice is wrong"),
229 ServiceError::NoAPIResponse => write!(f, "Price API not answered - retry"),
230 ServiceError::NoCurrency => write!(f, "Currency requested is not present in the exchange list, please specify a fixed rate"),
231 ServiceError::PriceTooStale => write!(f, "Exchange rate is too stale to price an order - retry or use a fixed rate"),
232 ServiceError::MalformedAPIRes => write!(f, "Malformed answer from exchange quoting request"),
233 ServiceError::NegativeAmount => write!(f, "Negative amount is not valid"),
234 ServiceError::LnAddressWrongAmount => write!(f, "Ln address need amount of 0 sats - please check your order"),
235 ServiceError::LnAddressParseError => write!(f, "Ln address parsing error - please check your address"),
236 ServiceError::LnPaymentError(e) => write!(f, "Lightning payment failure cause: {}",e),
237 ServiceError::LnNodeError(e) => write!(f, "Lightning node connection failure caused by: {}",e),
238 ServiceError::InvalidOrderId => write!(f, "Order id not present in database"),
239 ServiceError::InvalidPubkey => write!(f, "Invalid pubkey"),
240 ServiceError::DbAccessError(e) => write!(f, "Error in database access: {}",e),
241 ServiceError::HoldInvoiceError(e) => write!(f, "Error holding invoice: {}",e),
242 ServiceError::UpdateOrderStatusError => write!(f, "Error updating order status"),
243 ServiceError::InvalidOrderStatus => write!(f, "Invalid order status"),
244 ServiceError::InvalidOrderKind => write!(f, "Invalid order kind"),
245 ServiceError::DisputeAlreadyExists => write!(f, "Dispute already exists"),
246 ServiceError::DisputeEventError => write!(f, "Error publishing dispute event"),
247 ServiceError::NostrError(e) => write!(f, "Error in nostr: {}",e),
248 ServiceError::InvalidRating => write!(f, "Invalid rating message"),
249 ServiceError::InvalidRatingValue => write!(f, "Invalid rating value"),
250 ServiceError::MessageSerializationError => write!(f, "Error serializing message"),
251 ServiceError::InvalidDisputeId => write!(f, "Invalid dispute id"),
252 ServiceError::InvalidDisputeStatus => write!(f, "Invalid dispute status"),
253 ServiceError::InvalidPayload => write!(f, "Invalid payload"),
254 ServiceError::UnexpectedError(e) => write!(f, "Unexpected error: {}", e),
255 ServiceError::EnvVarError(e) => write!(f, "Environment variable error: {}", e),
256 ServiceError::IOError(e) => write!(f, "IO error: {}", e),
257 ServiceError::EncryptionError(e) => write!(f, "Encryption error: {}", e),
258 ServiceError::DecryptionError(e) => write!(f, "Decryption error: {}", e),
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn invalid_payload_serializes_to_snake_case() {
269 let json = serde_json::to_string(&CantDoReason::InvalidPayload).unwrap();
270 assert_eq!(json, "\"invalid_payload\"");
271 let round: CantDoReason = serde_json::from_str(&json).unwrap();
272 assert_eq!(round, CantDoReason::InvalidPayload);
273 }
274
275 #[test]
276 fn price_too_stale_serializes_to_snake_case() {
277 let json = serde_json::to_string(&CantDoReason::PriceTooStale).unwrap();
278 assert_eq!(json, "\"price_too_stale\"");
279 let round: CantDoReason = serde_json::from_str(&json).unwrap();
280 assert_eq!(round, CantDoReason::PriceTooStale);
281 }
282}