Skip to main content

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}