Skip to main content

cdk_common/
error.rs

1//! Errors
2
3use std::array::TryFromSliceError;
4use std::fmt;
5
6use cashu::{CurrencyUnit, PaymentMethod};
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use serde_json::Value;
9use thiserror::Error;
10
11use crate::nuts::Id;
12use crate::util::hex;
13#[cfg(feature = "wallet")]
14use crate::wallet::WalletKey;
15use crate::Amount;
16
17/// CDK Error
18#[derive(Debug, Error)]
19pub enum Error {
20    /// Mint does not have a key for amount
21    #[error("No Key for Amount")]
22    AmountKey,
23    /// Keyset is not known
24    #[error("Keyset id not known: `{0}`")]
25    KeysetUnknown(Id),
26    /// Unsupported unit
27    #[error("Unit unsupported")]
28    UnsupportedUnit,
29    /// Payment failed
30    #[error("Payment failed")]
31    PaymentFailed,
32    /// Payment pending
33    #[error("Payment pending")]
34    PaymentPending,
35    /// Invoice already paid
36    #[error("Request already paid")]
37    RequestAlreadyPaid,
38    /// Invalid payment request
39    #[error("Invalid payment request")]
40    InvalidPaymentRequest,
41    /// Bolt11 invoice does not have amount
42    #[error("Invoice Amount undefined")]
43    InvoiceAmountUndefined,
44    /// Split Values must be less then or equal to amount
45    #[error("Split Values must be less then or equal to amount")]
46    SplitValuesGreater,
47    /// Amount overflow
48    #[error("Amount Overflow")]
49    AmountOverflow,
50    /// Over issue - tried to issue more than paid
51    #[error("Cannot issue more than amount paid")]
52    OverIssue,
53    /// Witness missing or invalid
54    #[error("Signature missing or invalid")]
55    SignatureMissingOrInvalid,
56    /// Amountless Invoice Not supported
57    #[error("Amount Less Invoice is not allowed")]
58    AmountLessNotAllowed,
59    /// Multi-Part Internal Melt Quotes are not supported
60    #[error("Multi-Part Internal Melt Quotes are not supported")]
61    InternalMultiPartMeltQuote,
62    /// Multi-Part Payment not supported for unit and method
63    #[error("Multi-Part payment is not supported for unit `{0}` and method `{1}`")]
64    MppUnitMethodNotSupported(CurrencyUnit, PaymentMethod),
65    /// Clear Auth Required
66    #[error("Clear Auth Required")]
67    ClearAuthRequired,
68    /// Blind Auth Required
69    #[error("Blind Auth Required")]
70    BlindAuthRequired,
71    /// Clear Auth Failed
72    #[error("Clear Auth Failed")]
73    ClearAuthFailed,
74    /// Blind Auth Failed
75    #[error("Blind Auth Failed")]
76    BlindAuthFailed,
77    /// Auth settings undefined
78    #[error("Auth settings undefined")]
79    AuthSettingsUndefined,
80    /// Mint time outside of tolerance
81    #[error("Mint time outside of tolerance")]
82    MintTimeExceedsTolerance,
83    /// Insufficient blind auth tokens
84    #[error("Insufficient blind auth tokens, must reauth")]
85    InsufficientBlindAuthTokens,
86    /// Auth localstore undefined
87    #[error("Auth localstore undefined")]
88    AuthLocalstoreUndefined,
89    /// Wallet cat not set
90    #[error("Wallet cat not set")]
91    CatNotSet,
92    /// Could not get mint info
93    #[error("Could not get mint info")]
94    CouldNotGetMintInfo,
95    /// Multi-Part Payment not supported for unit and method
96    #[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")]
97    AmountlessInvoiceNotSupported(CurrencyUnit, PaymentMethod),
98    /// Duplicate Payment id
99    #[error("Payment id seen for mint")]
100    DuplicatePaymentId,
101    /// Pubkey required
102    #[error("Pubkey required")]
103    PubkeyRequired,
104    /// Invalid payment method
105    #[error("Invalid payment method")]
106    InvalidPaymentMethod,
107    /// Amount undefined
108    #[error("Amount undefined")]
109    AmountUndefined,
110    /// Unsupported payment method
111    #[error("Payment method unsupported")]
112    UnsupportedPaymentMethod,
113    /// Payment method required
114    #[error("Payment method required")]
115    PaymentMethodRequired,
116    /// Could not parse bolt12
117    #[error("Could not parse bolt12")]
118    Bolt12parse,
119    /// Could not parse invoice (bolt11 or bolt12)
120    #[error("Could not parse invoice")]
121    InvalidInvoice,
122
123    /// BIP353 address parsing error
124    #[error("Failed to parse BIP353 address: {0}")]
125    Bip353Parse(String),
126
127    /// Operation timeout
128    #[error("Operation timeout")]
129    Timeout,
130
131    /// BIP353 address resolution error
132    #[error("Failed to resolve BIP353 address: {0}")]
133    Bip353Resolve(String),
134    /// BIP353 no Lightning offer found
135    #[error("No Lightning offer found in BIP353 payment instructions")]
136    Bip353NoLightningOffer,
137
138    /// Lightning Address parsing error
139    #[error("Failed to parse Lightning address: {0}")]
140    LightningAddressParse(String),
141    /// Lightning Address request error
142    #[error("Failed to request invoice from Lightning address service: {0}")]
143    LightningAddressRequest(String),
144
145    /// Internal Error - Send error
146    #[error("Internal send error: {0}")]
147    SendError(String),
148
149    /// Internal Error - Recv error
150    #[error("Internal receive error: {0}")]
151    RecvError(String),
152
153    // Mint Errors
154    /// Minting is disabled
155    #[error("Minting is disabled")]
156    MintingDisabled,
157    /// Quote is not known
158    #[error("Unknown quote")]
159    UnknownQuote,
160    /// Quote is expired
161    #[error("Expired quote: Expired: `{0}`, Time: `{1}`")]
162    ExpiredQuote(u64, u64),
163    /// Amount is outside of allowed range
164    #[error("Amount must be between `{0}` and `{1}` is `{2}`")]
165    AmountOutofLimitRange(Amount, Amount, Amount),
166    /// Quote is not paid
167    #[error("Quote not paid")]
168    UnpaidQuote,
169    /// Quote is pending
170    #[error("Quote pending")]
171    PendingQuote,
172    /// ecash already issued for quote
173    #[error("Quote already issued")]
174    IssuedQuote,
175    /// Quote has already been paid
176    #[error("Quote is already paid")]
177    PaidQuote,
178    /// Payment state is unknown
179    #[error("Payment state is unknown")]
180    UnknownPaymentState,
181    /// Melting is disabled
182    #[error("Melting is disabled")]
183    MeltingDisabled,
184    /// Unknown Keyset
185    #[error("Unknown Keyset")]
186    UnknownKeySet,
187    /// BlindedMessage is already signed
188    #[error("Blinded Message is already signed")]
189    BlindedMessageAlreadySigned,
190    /// Inactive Keyset
191    #[error("Inactive Keyset")]
192    InactiveKeyset,
193    /// Transaction unbalanced
194    #[error("Inputs: `{0}`, Outputs: `{1}`, Expected Fee: `{2}`")]
195    TransactionUnbalanced(u64, u64, u64),
196    /// Duplicate proofs provided
197    #[error("Duplicate Inputs")]
198    DuplicateInputs,
199    /// Duplicate output
200    #[error("Duplicate outputs")]
201    DuplicateOutputs,
202    /// Maximum number of inputs exceeded
203    #[error("Maximum inputs exceeded: {actual} provided, max {max}")]
204    MaxInputsExceeded {
205        /// Actual number of inputs provided
206        actual: usize,
207        /// Maximum allowed inputs
208        max: usize,
209    },
210    /// Maximum number of outputs exceeded
211    #[error("Maximum outputs exceeded: {actual} provided, max {max}")]
212    MaxOutputsExceeded {
213        /// Actual number of outputs provided
214        actual: usize,
215        /// Maximum allowed outputs
216        max: usize,
217    },
218    /// Multiple units provided
219    #[error("Cannot have multiple units")]
220    MultipleUnits,
221    /// Unit mismatch
222    #[error("Input unit must match output")]
223    UnitMismatch,
224    /// Sig all cannot be used in melt
225    #[error("Sig all cannot be used in melt")]
226    SigAllUsedInMelt,
227    /// Token is already spent
228    #[error("Token Already Spent")]
229    TokenAlreadySpent,
230    /// Token is already pending
231    #[error("Token Pending")]
232    TokenPending,
233    /// Internal Error
234    #[error("Internal Error")]
235    Internal,
236    /// Oidc config not set
237    #[error("Oidc client not set")]
238    OidcNotSet,
239
240    // Wallet Errors
241    /// P2PK spending conditions not met
242    #[error("P2PK condition not met `{0}`")]
243    P2PKConditionsNotMet(String),
244    /// Duplicate signature from same pubkey in P2PK
245    #[error("Duplicate signature from same pubkey in P2PK")]
246    DuplicateSignatureError,
247    /// Spending Locktime not provided
248    #[error("Spending condition locktime not provided")]
249    LocktimeNotProvided,
250    /// Invalid Spending Conditions
251    #[error("Invalid spending conditions: `{0}`")]
252    InvalidSpendConditions(String),
253    /// Incorrect Wallet
254    #[error("Incorrect wallet: `{0}`")]
255    IncorrectWallet(String),
256    /// Unknown Wallet
257    #[error("Unknown wallet: `{0}`")]
258    #[cfg(feature = "wallet")]
259    UnknownWallet(WalletKey),
260    /// Max Fee Ecxeded
261    #[error("Max fee exceeded")]
262    MaxFeeExceeded,
263    /// Url path segments could not be joined
264    #[error("Url path segments could not be joined")]
265    UrlPathSegments,
266    ///  Unknown error response
267    #[error("Unknown error response: `{0}`")]
268    UnknownErrorResponse(String),
269    /// Invalid DLEQ proof
270    #[error("Could not verify DLEQ proof")]
271    CouldNotVerifyDleq,
272    /// Dleq Proof not provided for signature
273    #[error("Dleq proof not provided for signature")]
274    DleqProofNotProvided,
275    /// Incorrect Mint
276    /// Token does not match wallet mint
277    #[error("Token does not match wallet mint")]
278    IncorrectMint,
279    /// Receive can only be used with tokens from single mint
280    #[error("Multiple mint tokens not supported by receive. Please deconstruct the token and use receive with_proof")]
281    MultiMintTokenNotSupported,
282    /// Preimage not provided
283    #[error("Preimage not provided")]
284    PreimageNotProvided,
285
286    /// Unknown mint
287    #[error("Unknown mint: {mint_url}")]
288    UnknownMint {
289        /// URL of the unknown mint
290        mint_url: String,
291    },
292    /// Transfer between mints timed out
293    #[error("Transfer timeout: failed to transfer {amount} from {source_mint} to {target_mint}")]
294    TransferTimeout {
295        /// Source mint URL
296        source_mint: String,
297        /// Target mint URL
298        target_mint: String,
299        /// Amount that failed to transfer
300        amount: Amount,
301    },
302    /// Insufficient Funds
303    #[error("Insufficient funds")]
304    InsufficientFunds,
305    /// Unexpected proof state
306    #[error("Unexpected proof state")]
307    UnexpectedProofState,
308    /// No active keyset
309    #[error("No active keyset")]
310    NoActiveKeyset,
311    /// Incorrect quote amount
312    #[error("Incorrect quote amount")]
313    IncorrectQuoteAmount,
314    /// Invoice Description not supported
315    #[error("Invoice Description not supported")]
316    InvoiceDescriptionUnsupported,
317    /// Invalid transaction direction
318    #[error("Invalid transaction direction")]
319    InvalidTransactionDirection,
320    /// Invalid transaction id
321    #[error("Invalid transaction id")]
322    InvalidTransactionId,
323    /// Transaction not found
324    #[error("Transaction not found")]
325    TransactionNotFound,
326    /// Invalid operation kind
327    #[error("Invalid operation kind")]
328    InvalidOperationKind,
329    /// Invalid operation state
330    #[error("Invalid operation state")]
331    InvalidOperationState,
332    /// Operation not found
333    #[error("Operation not found")]
334    OperationNotFound,
335    /// KV Store invalid key or namespace
336    #[error("Invalid KV store key or namespace: {0}")]
337    KVStoreInvalidKey(String),
338    /// Concurrent update detected
339    #[error("Concurrent update detected")]
340    ConcurrentUpdate,
341    /// Invalid response from mint
342    #[error("Invalid mint response: {0}")]
343    InvalidMintResponse(String),
344    /// Subscription error
345    #[error("Subscription error: {0}")]
346    SubscriptionError(String),
347    /// Custom Error
348    #[error("`{0}`")]
349    Custom(String),
350
351    // External Error conversions
352    /// Parse invoice error
353    #[error(transparent)]
354    Invoice(#[from] lightning_invoice::ParseOrSemanticError),
355    /// Bip32 error
356    #[error(transparent)]
357    Bip32(#[from] bitcoin::bip32::Error),
358    /// Parse int error
359    #[error(transparent)]
360    ParseInt(#[from] std::num::ParseIntError),
361    /// Parse 9rl Error
362    #[error(transparent)]
363    UrlParseError(#[from] url::ParseError),
364    /// Utf8 parse error
365    #[error(transparent)]
366    Utf8ParseError(#[from] std::string::FromUtf8Error),
367    /// Serde Json error
368    #[error(transparent)]
369    SerdeJsonError(#[from] serde_json::Error),
370    /// Base64 error
371    #[error(transparent)]
372    Base64Error(#[from] bitcoin::base64::DecodeError),
373    /// From hex error
374    #[error(transparent)]
375    HexError(#[from] hex::Error),
376    /// Http transport error
377    #[error("Http transport error {0:?}: {1}")]
378    HttpError(Option<u16>, String),
379    /// Parse invoice error
380    #[cfg(feature = "mint")]
381    #[error(transparent)]
382    Uuid(#[from] uuid::Error),
383    // Crate error conversions
384    /// Cashu Url Error
385    #[error(transparent)]
386    CashuUrl(#[from] crate::mint_url::Error),
387    /// Secret error
388    #[error(transparent)]
389    Secret(#[from] crate::secret::Error),
390    /// Amount Error
391    #[error(transparent)]
392    AmountError(#[from] crate::amount::Error),
393    /// DHKE Error
394    #[error(transparent)]
395    DHKE(#[from] crate::dhke::Error),
396    /// NUT00 Error
397    #[error(transparent)]
398    NUT00(#[from] crate::nuts::nut00::Error),
399    /// Nut01 error
400    #[error(transparent)]
401    NUT01(#[from] crate::nuts::nut01::Error),
402    /// NUT02 error
403    #[error(transparent)]
404    NUT02(#[from] crate::nuts::nut02::Error),
405    /// NUT03 error
406    #[error(transparent)]
407    NUT03(#[from] crate::nuts::nut03::Error),
408    /// NUT04 error
409    #[error(transparent)]
410    NUT04(#[from] crate::nuts::nut04::Error),
411    /// NUT05 error
412    #[error(transparent)]
413    NUT05(#[from] crate::nuts::nut05::Error),
414    /// NUT11 Error
415    #[error(transparent)]
416    NUT11(#[from] crate::nuts::nut11::Error),
417    /// NUT12 Error
418    #[error(transparent)]
419    NUT12(#[from] crate::nuts::nut12::Error),
420    /// NUT13 Error
421    #[error(transparent)]
422    #[cfg(feature = "wallet")]
423    NUT13(#[from] crate::nuts::nut13::Error),
424    /// NUT14 Error
425    #[error(transparent)]
426    NUT14(#[from] crate::nuts::nut14::Error),
427    /// NUT18 Error
428    #[error(transparent)]
429    NUT18(#[from] crate::nuts::nut18::Error),
430    /// NUT20 Error
431    #[error(transparent)]
432    NUT20(#[from] crate::nuts::nut20::Error),
433    /// NUT21 Error
434    #[error(transparent)]
435    NUT21(#[from] crate::nuts::nut21::Error),
436    /// NUT22 Error
437    #[error(transparent)]
438    NUT22(#[from] crate::nuts::nut22::Error),
439    /// NUT23 Error
440    #[error(transparent)]
441    NUT23(#[from] crate::nuts::nut23::Error),
442    /// Quote ID Error
443    #[error(transparent)]
444    #[cfg(feature = "mint")]
445    QuoteId(#[from] crate::quote_id::QuoteIdError),
446    /// From slice error
447    #[error(transparent)]
448    TryFromSliceError(#[from] TryFromSliceError),
449    /// Database Error
450    #[error(transparent)]
451    Database(crate::database::Error),
452    /// Payment Error
453    #[error(transparent)]
454    #[cfg(feature = "mint")]
455    Payment(#[from] crate::payment::Error),
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461
462    #[test]
463    fn test_is_definitive_failure() {
464        // Test definitive failures
465        assert!(Error::AmountOverflow.is_definitive_failure());
466        assert!(Error::TokenAlreadySpent.is_definitive_failure());
467        assert!(Error::MintingDisabled.is_definitive_failure());
468
469        // Test HTTP client errors (4xx) - simulated
470        assert!(Error::HttpError(Some(400), "Bad Request".to_string()).is_definitive_failure());
471        assert!(Error::HttpError(Some(404), "Not Found".to_string()).is_definitive_failure());
472        assert!(
473            Error::HttpError(Some(429), "Too Many Requests".to_string()).is_definitive_failure()
474        );
475
476        // Test ambiguous failures
477        assert!(!Error::Timeout.is_definitive_failure());
478        assert!(!Error::Internal.is_definitive_failure());
479        assert!(!Error::ConcurrentUpdate.is_definitive_failure());
480
481        // Test HTTP server errors (5xx)
482        assert!(
483            !Error::HttpError(Some(500), "Internal Server Error".to_string())
484                .is_definitive_failure()
485        );
486        assert!(!Error::HttpError(Some(502), "Bad Gateway".to_string()).is_definitive_failure());
487        assert!(
488            !Error::HttpError(Some(503), "Service Unavailable".to_string()).is_definitive_failure()
489        );
490
491        // Test HTTP network errors (no status)
492        assert!(!Error::HttpError(None, "Connection refused".to_string()).is_definitive_failure());
493    }
494}
495
496impl Error {
497    /// Check if the error is a definitive failure
498    ///
499    /// A definitive failure means the mint definitely rejected the request
500    /// and did not update its state. In these cases, it is safe to revert
501    /// the transaction locally.
502    ///
503    /// If false, the failure is ambiguous (e.g. timeout, network error, 500)
504    /// and the transaction state at the mint is unknown.
505    pub fn is_definitive_failure(&self) -> bool {
506        match self {
507            // Logic/Validation Errors (Safe to revert)
508            Self::AmountKey
509            | Self::KeysetUnknown(_)
510            | Self::UnsupportedUnit
511            | Self::InvoiceAmountUndefined
512            | Self::SplitValuesGreater
513            | Self::AmountOverflow
514            | Self::OverIssue
515            | Self::SignatureMissingOrInvalid
516            | Self::AmountLessNotAllowed
517            | Self::InternalMultiPartMeltQuote
518            | Self::MppUnitMethodNotSupported(_, _)
519            | Self::AmountlessInvoiceNotSupported(_, _)
520            | Self::DuplicatePaymentId
521            | Self::PubkeyRequired
522            | Self::InvalidPaymentMethod
523            | Self::UnsupportedPaymentMethod
524            | Self::InvalidInvoice
525            | Self::MintingDisabled
526            | Self::UnknownQuote
527            | Self::ExpiredQuote(_, _)
528            | Self::AmountOutofLimitRange(_, _, _)
529            | Self::UnpaidQuote
530            | Self::PendingQuote
531            | Self::IssuedQuote
532            | Self::PaidQuote
533            | Self::MeltingDisabled
534            | Self::UnknownKeySet
535            | Self::BlindedMessageAlreadySigned
536            | Self::InactiveKeyset
537            | Self::TransactionUnbalanced(_, _, _)
538            | Self::DuplicateInputs
539            | Self::DuplicateOutputs
540            | Self::MultipleUnits
541            | Self::UnitMismatch
542            | Self::SigAllUsedInMelt
543            | Self::TokenAlreadySpent
544            | Self::TokenPending
545            | Self::P2PKConditionsNotMet(_)
546            | Self::DuplicateSignatureError
547            | Self::LocktimeNotProvided
548            | Self::InvalidSpendConditions(_)
549            | Self::IncorrectWallet(_)
550            | Self::MaxFeeExceeded
551            | Self::DleqProofNotProvided
552            | Self::IncorrectMint
553            | Self::MultiMintTokenNotSupported
554            | Self::PreimageNotProvided
555            | Self::UnknownMint { .. }
556            | Self::UnexpectedProofState
557            | Self::NoActiveKeyset
558            | Self::IncorrectQuoteAmount
559            | Self::InvoiceDescriptionUnsupported
560            | Self::InvalidTransactionDirection
561            | Self::InvalidTransactionId
562            | Self::InvalidOperationKind
563            | Self::InvalidOperationState
564            | Self::OperationNotFound
565            | Self::KVStoreInvalidKey(_) => true,
566
567            // HTTP Errors
568            Self::HttpError(Some(status), _) => {
569                // Client errors (400-499) are definitive failures
570                // Server errors (500-599) are ambiguous
571                (400..500).contains(status)
572            }
573
574            // Ambiguous Errors (Unsafe to revert)
575            Self::Timeout
576            | Self::Internal
577            | Self::UnknownPaymentState
578            | Self::CouldNotGetMintInfo
579            | Self::UnknownErrorResponse(_)
580            | Self::InvalidMintResponse(_)
581            | Self::ConcurrentUpdate
582            | Self::SendError(_)
583            | Self::RecvError(_)
584            | Self::TransferTimeout { .. } => false,
585
586            // Network/IO/Parsing Errors (Usually ambiguous as they could happen reading response)
587            Self::HttpError(None, _) // No status code means network error
588            | Self::SerdeJsonError(_) // Could be malformed success response
589            | Self::Database(_)
590            | Self::Custom(_) => false,
591
592            // Auth Errors (Generally definitive if rejected)
593            Self::ClearAuthRequired
594            | Self::BlindAuthRequired
595            | Self::ClearAuthFailed
596            | Self::BlindAuthFailed
597            | Self::InsufficientBlindAuthTokens
598            | Self::AuthSettingsUndefined
599            | Self::AuthLocalstoreUndefined
600            | Self::OidcNotSet => true,
601
602            // External conversions - check specifically
603            Self::Invoice(_) => true, // Parsing error
604            Self::Bip32(_) => true, // Key derivation error
605            Self::ParseInt(_) => true,
606            Self::UrlParseError(_) => true,
607            Self::Utf8ParseError(_) => true,
608            Self::Base64Error(_) => true,
609            Self::HexError(_) => true,
610            #[cfg(feature = "mint")]
611            Self::Uuid(_) => true,
612            Self::CashuUrl(_) => true,
613            Self::Secret(_) => true,
614            Self::AmountError(_) => true,
615            Self::DHKE(_) => true, // Crypto errors
616            Self::NUT00(_) => true,
617            Self::NUT01(_) => true,
618            Self::NUT02(_) => true,
619            Self::NUT03(_) => true,
620            Self::NUT04(_) => true,
621            Self::NUT05(_) => true,
622            Self::NUT11(_) => true,
623            Self::NUT12(_) => true,
624            #[cfg(feature = "wallet")]
625            Self::NUT13(_) => true,
626            Self::NUT14(_) => true,
627            Self::NUT18(_) => true,
628            Self::NUT20(_) => true,
629            Self::NUT21(_) => true,
630            Self::NUT22(_) => true,
631            Self::NUT23(_) => true,
632            #[cfg(feature = "mint")]
633            Self::QuoteId(_) => true,
634            Self::TryFromSliceError(_) => true,
635            #[cfg(feature = "mint")]
636            Self::Payment(_) => false, // Payment errors could be ambiguous? assume ambiguous to be safe
637
638            // Catch-all
639            _ => false,
640        }
641    }
642}
643
644/// CDK Error Response
645///
646/// See NUT definition in [00](https://github.com/cashubtc/nuts/blob/main/00.md)
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
649pub struct ErrorResponse {
650    /// Error Code
651    pub code: ErrorCode,
652    /// Human readable description
653    #[serde(default)]
654    pub detail: String,
655}
656
657impl fmt::Display for ErrorResponse {
658    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659        write!(f, "code: {}, detail: {}", self.code, self.detail)
660    }
661}
662
663impl ErrorResponse {
664    /// Create new [`ErrorResponse`]
665    pub fn new(code: ErrorCode, detail: String) -> Self {
666        Self { code, detail }
667    }
668
669    /// Error response from json
670    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
671        let value: Value = serde_json::from_str(json)?;
672
673        Self::from_value(value)
674    }
675
676    /// Error response from json Value
677    pub fn from_value(value: Value) -> Result<Self, serde_json::Error> {
678        match serde_json::from_value::<ErrorResponse>(value.clone()) {
679            Ok(res) => Ok(res),
680            Err(_) => Ok(Self {
681                code: ErrorCode::Unknown(999),
682                detail: value.to_string(),
683            }),
684        }
685    }
686}
687
688/// Maps NUT11 errors to appropriate error codes
689/// All NUT11 errors are witness/signature related, so they map to WitnessMissingOrInvalid (20008)
690fn map_nut11_error(_nut11_error: &crate::nuts::nut11::Error) -> ErrorCode {
691    // All NUT11 errors relate to P2PK/witness validation, which maps to 20008
692    ErrorCode::WitnessMissingOrInvalid
693}
694
695impl From<Error> for ErrorResponse {
696    fn from(err: Error) -> ErrorResponse {
697        match err {
698            Error::TokenAlreadySpent => ErrorResponse {
699                code: ErrorCode::TokenAlreadySpent,
700                detail: err.to_string(),
701            },
702            Error::UnsupportedUnit => ErrorResponse {
703                code: ErrorCode::UnsupportedUnit,
704                detail: err.to_string(),
705            },
706            Error::PaymentFailed => ErrorResponse {
707                code: ErrorCode::LightningError,
708                detail: err.to_string(),
709            },
710            Error::RequestAlreadyPaid => ErrorResponse {
711                code: ErrorCode::InvoiceAlreadyPaid,
712                detail: "Invoice already paid.".to_string(),
713            },
714            Error::TransactionUnbalanced(inputs_total, outputs_total, fee_expected) => {
715                ErrorResponse {
716                    code: ErrorCode::TransactionUnbalanced,
717                    detail: format!(
718                        "Inputs: {inputs_total}, Outputs: {outputs_total}, expected_fee: {fee_expected}. Transaction inputs should equal outputs less fee"
719                    ),
720                }
721            }
722            Error::MintingDisabled => ErrorResponse {
723                code: ErrorCode::MintingDisabled,
724                detail: err.to_string(),
725            },
726            Error::BlindedMessageAlreadySigned => ErrorResponse {
727                code: ErrorCode::BlindedMessageAlreadySigned,
728                detail: err.to_string(),
729            },
730            Error::InsufficientFunds => ErrorResponse {
731                code: ErrorCode::TransactionUnbalanced,
732                detail: err.to_string(),
733            },
734            Error::AmountOutofLimitRange(_min, _max, _amount) => ErrorResponse {
735                code: ErrorCode::AmountOutofLimitRange,
736                detail: err.to_string(),
737            },
738            Error::ExpiredQuote(_, _) => ErrorResponse {
739                code: ErrorCode::QuoteExpired,
740                detail: err.to_string(),
741            },
742            Error::PendingQuote => ErrorResponse {
743                code: ErrorCode::QuotePending,
744                detail: err.to_string(),
745            },
746            Error::TokenPending => ErrorResponse {
747                code: ErrorCode::TokenPending,
748                detail: err.to_string(),
749            },
750            Error::ClearAuthRequired => ErrorResponse {
751                code: ErrorCode::ClearAuthRequired,
752                detail: Error::ClearAuthRequired.to_string(),
753            },
754            Error::ClearAuthFailed => ErrorResponse {
755                code: ErrorCode::ClearAuthFailed,
756                detail: Error::ClearAuthFailed.to_string(),
757            },
758            Error::BlindAuthRequired => ErrorResponse {
759                code: ErrorCode::BlindAuthRequired,
760                detail: Error::BlindAuthRequired.to_string(),
761            },
762            Error::BlindAuthFailed => ErrorResponse {
763                code: ErrorCode::BlindAuthFailed,
764                detail: Error::BlindAuthFailed.to_string(),
765            },
766            Error::NUT20(err) => ErrorResponse {
767                code: ErrorCode::WitnessMissingOrInvalid,
768                detail: err.to_string(),
769            },
770            Error::DuplicateInputs => ErrorResponse {
771                code: ErrorCode::DuplicateInputs,
772                detail: err.to_string(),
773            },
774            Error::DuplicateOutputs => ErrorResponse {
775                code: ErrorCode::DuplicateOutputs,
776                detail: err.to_string(),
777            },
778            Error::MultipleUnits => ErrorResponse {
779                code: ErrorCode::MultipleUnits,
780                detail: err.to_string(),
781            },
782            Error::UnitMismatch => ErrorResponse {
783                code: ErrorCode::UnitMismatch,
784                detail: err.to_string(),
785            },
786            Error::UnpaidQuote => ErrorResponse {
787                code: ErrorCode::QuoteNotPaid,
788                detail: Error::UnpaidQuote.to_string(),
789            },
790            Error::NUT11(err) => {
791                let code = map_nut11_error(&err);
792                let extra = if matches!(err, crate::nuts::nut11::Error::SignaturesNotProvided) {
793                    Some("P2PK signatures are required but not provided".to_string())
794                } else {
795                    None
796                };
797                ErrorResponse {
798                    code,
799                    detail: match extra {
800                        Some(extra) => format!("{err}. {extra}"),
801                        None => err.to_string(),
802                    },
803                }
804            },
805            Error::DuplicateSignatureError => ErrorResponse {
806                code: ErrorCode::WitnessMissingOrInvalid,
807                detail: err.to_string(),
808            },
809            Error::IssuedQuote => ErrorResponse {
810                code: ErrorCode::TokensAlreadyIssued,
811                detail: err.to_string(),
812            },
813            Error::UnknownKeySet => ErrorResponse {
814                code: ErrorCode::KeysetNotFound,
815                detail: err.to_string(),
816            },
817            Error::InactiveKeyset => ErrorResponse {
818                code: ErrorCode::KeysetInactive,
819                detail: err.to_string(),
820            },
821            Error::AmountLessNotAllowed => ErrorResponse {
822                code: ErrorCode::AmountlessInvoiceNotSupported,
823                detail: err.to_string(),
824            },
825            Error::IncorrectQuoteAmount => ErrorResponse {
826                code: ErrorCode::IncorrectQuoteAmount,
827                detail: err.to_string(),
828            },
829            Error::PubkeyRequired => ErrorResponse {
830                code: ErrorCode::PubkeyRequired,
831                detail: err.to_string(),
832            },
833            Error::PaidQuote => ErrorResponse {
834                code: ErrorCode::InvoiceAlreadyPaid,
835                detail: err.to_string(),
836            },
837            Error::DuplicatePaymentId => ErrorResponse {
838                code: ErrorCode::InvoiceAlreadyPaid,
839                detail: err.to_string(),
840            },
841            // Database duplicate error indicates another quote with same invoice is already pending/paid
842            Error::Database(crate::database::Error::Duplicate) => ErrorResponse {
843                code: ErrorCode::InvoiceAlreadyPaid,
844                detail: "Invoice already paid or pending".to_string(),
845            },
846
847            // DHKE errors - TokenNotVerified for actual verification failures
848            Error::DHKE(crate::dhke::Error::TokenNotVerified) => ErrorResponse {
849                code: ErrorCode::TokenNotVerified,
850                detail: err.to_string(),
851            },
852            Error::DHKE(_) => ErrorResponse {
853                code: ErrorCode::Unknown(50000),
854                detail: err.to_string(),
855            },
856
857            // Verification errors
858            Error::CouldNotVerifyDleq => ErrorResponse {
859                code: ErrorCode::TokenNotVerified,
860                detail: err.to_string(),
861            },
862            Error::SignatureMissingOrInvalid => ErrorResponse {
863                code: ErrorCode::WitnessMissingOrInvalid,
864                detail: err.to_string(),
865            },
866            Error::SigAllUsedInMelt => ErrorResponse {
867                code: ErrorCode::WitnessMissingOrInvalid,
868                detail: err.to_string(),
869            },
870
871            // Keyset/key errors
872            Error::AmountKey => ErrorResponse {
873                code: ErrorCode::KeysetNotFound,
874                detail: err.to_string(),
875            },
876            Error::KeysetUnknown(_) => ErrorResponse {
877                code: ErrorCode::KeysetNotFound,
878                detail: err.to_string(),
879            },
880            Error::NoActiveKeyset => ErrorResponse {
881                code: ErrorCode::KeysetInactive,
882                detail: err.to_string(),
883            },
884
885            // Quote/payment errors
886            Error::UnknownQuote => ErrorResponse {
887                code: ErrorCode::Unknown(50000),
888                detail: err.to_string(),
889            },
890            Error::MeltingDisabled => ErrorResponse {
891                code: ErrorCode::MintingDisabled,
892                detail: err.to_string(),
893            },
894            Error::PaymentPending => ErrorResponse {
895                code: ErrorCode::QuotePending,
896                detail: err.to_string(),
897            },
898            Error::UnknownPaymentState => ErrorResponse {
899                code: ErrorCode::Unknown(50000),
900                detail: err.to_string(),
901            },
902
903            // Transaction/amount errors
904            Error::SplitValuesGreater => ErrorResponse {
905                code: ErrorCode::TransactionUnbalanced,
906                detail: err.to_string(),
907            },
908            Error::AmountOverflow => ErrorResponse {
909                code: ErrorCode::TransactionUnbalanced,
910                detail: err.to_string(),
911            },
912            Error::OverIssue => ErrorResponse {
913                code: ErrorCode::TransactionUnbalanced,
914                detail: err.to_string(),
915            },
916
917            // Invoice parsing errors - no spec code for invalid format
918            Error::InvalidPaymentRequest => ErrorResponse {
919                code: ErrorCode::Unknown(50000),
920                detail: err.to_string(),
921            },
922            Error::InvoiceAmountUndefined => ErrorResponse {
923                code: ErrorCode::AmountlessInvoiceNotSupported,
924                detail: err.to_string(),
925            },
926
927            // Internal/system errors - use Unknown(99999)
928            Error::Internal => ErrorResponse {
929                code: ErrorCode::Unknown(50000),
930                detail: err.to_string(),
931            },
932            Error::Database(_) => ErrorResponse {
933                code: ErrorCode::Unknown(50000),
934                detail: err.to_string(),
935            },
936            Error::ConcurrentUpdate => ErrorResponse {
937                code: ErrorCode::ConcurrentUpdate,
938                detail: err.to_string(),
939            },
940            Error::MaxInputsExceeded { .. } => ErrorResponse {
941                code: ErrorCode::MaxInputsExceeded,
942                detail: err.to_string()
943            },
944            Error::MaxOutputsExceeded { .. } => ErrorResponse {
945                code: ErrorCode::MaxOutputsExceeded,
946                detail: err.to_string()
947            },
948            // Fallback for any remaining errors - use Unknown(99999) instead of TokenNotVerified
949            _ => ErrorResponse {
950                code: ErrorCode::Unknown(50000),
951                detail: err.to_string(),
952            },
953        }
954    }
955}
956
957#[cfg(feature = "mint")]
958impl From<crate::database::Error> for Error {
959    fn from(db_error: crate::database::Error) -> Self {
960        match db_error {
961            crate::database::Error::InvalidStateTransition(state) => match state {
962                crate::state::Error::Pending => Self::TokenPending,
963                crate::state::Error::AlreadySpent => Self::TokenAlreadySpent,
964                crate::state::Error::AlreadyPaid => Self::RequestAlreadyPaid,
965                state => Self::Database(crate::database::Error::InvalidStateTransition(state)),
966            },
967            crate::database::Error::ConcurrentUpdate => Self::ConcurrentUpdate,
968            db_error => Self::Database(db_error),
969        }
970    }
971}
972
973#[cfg(not(feature = "mint"))]
974impl From<crate::database::Error> for Error {
975    fn from(db_error: crate::database::Error) -> Self {
976        match db_error {
977            crate::database::Error::ConcurrentUpdate => Self::ConcurrentUpdate,
978            db_error => Self::Database(db_error),
979        }
980    }
981}
982
983impl From<ErrorResponse> for Error {
984    fn from(err: ErrorResponse) -> Error {
985        match err.code {
986            // 10xxx - Proof/Token verification errors
987            ErrorCode::TokenNotVerified => Self::DHKE(crate::dhke::Error::TokenNotVerified),
988            // 11xxx - Input/Output errors
989            ErrorCode::TokenAlreadySpent => Self::TokenAlreadySpent,
990            ErrorCode::TokenPending => Self::TokenPending,
991            ErrorCode::BlindedMessageAlreadySigned => Self::BlindedMessageAlreadySigned,
992            ErrorCode::OutputsPending => Self::TokenPending, // Map to closest equivalent
993            ErrorCode::TransactionUnbalanced => Self::TransactionUnbalanced(0, 0, 0),
994            ErrorCode::AmountOutofLimitRange => {
995                Self::AmountOutofLimitRange(Amount::default(), Amount::default(), Amount::default())
996            }
997            ErrorCode::DuplicateInputs => Self::DuplicateInputs,
998            ErrorCode::DuplicateOutputs => Self::DuplicateOutputs,
999            ErrorCode::MultipleUnits => Self::MultipleUnits,
1000            ErrorCode::UnitMismatch => Self::UnitMismatch,
1001            ErrorCode::AmountlessInvoiceNotSupported => Self::AmountLessNotAllowed,
1002            ErrorCode::IncorrectQuoteAmount => Self::IncorrectQuoteAmount,
1003            ErrorCode::UnsupportedUnit => Self::UnsupportedUnit,
1004            // 12xxx - Keyset errors
1005            ErrorCode::KeysetNotFound => Self::UnknownKeySet,
1006            ErrorCode::KeysetInactive => Self::InactiveKeyset,
1007            // 20xxx - Quote/Payment errors
1008            ErrorCode::QuoteNotPaid => Self::UnpaidQuote,
1009            ErrorCode::TokensAlreadyIssued => Self::IssuedQuote,
1010            ErrorCode::MintingDisabled => Self::MintingDisabled,
1011            ErrorCode::LightningError => Self::PaymentFailed,
1012            ErrorCode::QuotePending => Self::PendingQuote,
1013            ErrorCode::InvoiceAlreadyPaid => Self::RequestAlreadyPaid,
1014            ErrorCode::QuoteExpired => Self::ExpiredQuote(0, 0),
1015            ErrorCode::WitnessMissingOrInvalid => Self::SignatureMissingOrInvalid,
1016            ErrorCode::PubkeyRequired => Self::PubkeyRequired,
1017            // 30xxx - Clear auth errors
1018            ErrorCode::ClearAuthRequired => Self::ClearAuthRequired,
1019            ErrorCode::ClearAuthFailed => Self::ClearAuthFailed,
1020            // 31xxx - Blind auth errors
1021            ErrorCode::BlindAuthRequired => Self::BlindAuthRequired,
1022            ErrorCode::BlindAuthFailed => Self::BlindAuthFailed,
1023            ErrorCode::BatMintMaxExceeded => Self::InsufficientBlindAuthTokens,
1024            ErrorCode::BatRateLimitExceeded => Self::InsufficientBlindAuthTokens,
1025            _ => Self::UnknownErrorResponse(err.to_string()),
1026        }
1027    }
1028}
1029
1030/// Possible Error Codes
1031#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
1032#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
1033pub enum ErrorCode {
1034    // 10xxx - Proof/Token verification errors
1035    /// Proof verification failed (10001)
1036    TokenNotVerified,
1037
1038    // 11xxx - Input/Output errors
1039    /// Proofs already spent (11001)
1040    TokenAlreadySpent,
1041    /// Proofs are pending (11002)
1042    TokenPending,
1043    /// Outputs already signed (11003)
1044    BlindedMessageAlreadySigned,
1045    /// Outputs are pending (11004)
1046    OutputsPending,
1047    /// Transaction is not balanced (11005)
1048    TransactionUnbalanced,
1049    /// Amount outside of limit range (11006)
1050    AmountOutofLimitRange,
1051    /// Duplicate inputs provided (11007)
1052    DuplicateInputs,
1053    /// Duplicate outputs provided (11008)
1054    DuplicateOutputs,
1055    /// Inputs/Outputs of multiple units (11009)
1056    MultipleUnits,
1057    /// Inputs and outputs not of same unit (11010)
1058    UnitMismatch,
1059    /// Amountless invoice is not supported (11011)
1060    AmountlessInvoiceNotSupported,
1061    /// Amount in request does not equal invoice (11012)
1062    IncorrectQuoteAmount,
1063    /// Unit in request is not supported (11013)
1064    UnsupportedUnit,
1065    /// The max number of inputs is exceeded
1066    MaxInputsExceeded,
1067    /// The max number of outputs is exceeded
1068    MaxOutputsExceeded,
1069
1070    // 12xxx - Keyset errors
1071    /// Keyset is not known (12001)
1072    KeysetNotFound,
1073    /// Keyset is inactive, cannot sign messages (12002)
1074    KeysetInactive,
1075
1076    // 20xxx - Quote/Payment errors
1077    /// Quote request is not paid (20001)
1078    QuoteNotPaid,
1079    /// Quote has already been issued (20002)
1080    TokensAlreadyIssued,
1081    /// Minting is disabled (20003)
1082    MintingDisabled,
1083    /// Lightning payment failed (20004)
1084    LightningError,
1085    /// Quote is pending (20005)
1086    QuotePending,
1087    /// Invoice already paid (20006)
1088    InvoiceAlreadyPaid,
1089    /// Quote is expired (20007)
1090    QuoteExpired,
1091    /// Signature for mint request invalid (20008)
1092    WitnessMissingOrInvalid,
1093    /// Pubkey required for mint quote (20009)
1094    PubkeyRequired,
1095
1096    // 30xxx - Clear auth errors
1097    /// Endpoint requires clear auth (30001)
1098    ClearAuthRequired,
1099    /// Clear authentication failed (30002)
1100    ClearAuthFailed,
1101
1102    // 31xxx - Blind auth errors
1103    /// Endpoint requires blind auth (31001)
1104    BlindAuthRequired,
1105    /// Blind authentication failed (31002)
1106    BlindAuthFailed,
1107    /// Maximum BAT mint amount exceeded (31003)
1108    BatMintMaxExceeded,
1109    /// BAT mint rate limit exceeded (31004)
1110    BatRateLimitExceeded,
1111
1112    /// Concurrent update detected
1113    ConcurrentUpdate,
1114
1115    /// Unknown error code
1116    Unknown(u16),
1117}
1118
1119impl ErrorCode {
1120    /// Error code from u16
1121    pub fn from_code(code: u16) -> Self {
1122        match code {
1123            // 10xxx - Proof/Token verification errors
1124            10001 => Self::TokenNotVerified,
1125            // 11xxx - Input/Output errors
1126            11001 => Self::TokenAlreadySpent,
1127            11002 => Self::TokenPending,
1128            11003 => Self::BlindedMessageAlreadySigned,
1129            11004 => Self::OutputsPending,
1130            11005 => Self::TransactionUnbalanced,
1131            11006 => Self::AmountOutofLimitRange,
1132            11007 => Self::DuplicateInputs,
1133            11008 => Self::DuplicateOutputs,
1134            11009 => Self::MultipleUnits,
1135            11010 => Self::UnitMismatch,
1136            11011 => Self::AmountlessInvoiceNotSupported,
1137            11012 => Self::IncorrectQuoteAmount,
1138            11013 => Self::UnsupportedUnit,
1139            11014 => Self::MaxInputsExceeded,
1140            11015 => Self::MaxOutputsExceeded,
1141            // 12xxx - Keyset errors
1142            12001 => Self::KeysetNotFound,
1143            12002 => Self::KeysetInactive,
1144            // 20xxx - Quote/Payment errors
1145            20001 => Self::QuoteNotPaid,
1146            20002 => Self::TokensAlreadyIssued,
1147            20003 => Self::MintingDisabled,
1148            20004 => Self::LightningError,
1149            20005 => Self::QuotePending,
1150            20006 => Self::InvoiceAlreadyPaid,
1151            20007 => Self::QuoteExpired,
1152            20008 => Self::WitnessMissingOrInvalid,
1153            20009 => Self::PubkeyRequired,
1154            // 30xxx - Clear auth errors
1155            30001 => Self::ClearAuthRequired,
1156            30002 => Self::ClearAuthFailed,
1157            // 31xxx - Blind auth errors
1158            31001 => Self::BlindAuthRequired,
1159            31002 => Self::BlindAuthFailed,
1160            31003 => Self::BatMintMaxExceeded,
1161            31004 => Self::BatRateLimitExceeded,
1162            _ => Self::Unknown(code),
1163        }
1164    }
1165
1166    /// Error code to u16
1167    pub fn to_code(&self) -> u16 {
1168        match self {
1169            // 10xxx - Proof/Token verification errors
1170            Self::TokenNotVerified => 10001,
1171            // 11xxx - Input/Output errors
1172            Self::TokenAlreadySpent => 11001,
1173            Self::TokenPending => 11002,
1174            Self::BlindedMessageAlreadySigned => 11003,
1175            Self::OutputsPending => 11004,
1176            Self::TransactionUnbalanced => 11005,
1177            Self::AmountOutofLimitRange => 11006,
1178            Self::DuplicateInputs => 11007,
1179            Self::DuplicateOutputs => 11008,
1180            Self::MultipleUnits => 11009,
1181            Self::UnitMismatch => 11010,
1182            Self::AmountlessInvoiceNotSupported => 11011,
1183            Self::IncorrectQuoteAmount => 11012,
1184            Self::UnsupportedUnit => 11013,
1185            Self::MaxInputsExceeded => 11014,
1186            Self::MaxOutputsExceeded => 11015,
1187            // 12xxx - Keyset errors
1188            Self::KeysetNotFound => 12001,
1189            Self::KeysetInactive => 12002,
1190            // 20xxx - Quote/Payment errors
1191            Self::QuoteNotPaid => 20001,
1192            Self::TokensAlreadyIssued => 20002,
1193            Self::MintingDisabled => 20003,
1194            Self::LightningError => 20004,
1195            Self::QuotePending => 20005,
1196            Self::InvoiceAlreadyPaid => 20006,
1197            Self::QuoteExpired => 20007,
1198            Self::WitnessMissingOrInvalid => 20008,
1199            Self::PubkeyRequired => 20009,
1200            // 30xxx - Clear auth errors
1201            Self::ClearAuthRequired => 30001,
1202            Self::ClearAuthFailed => 30002,
1203            // 31xxx - Blind auth errors
1204            Self::BlindAuthRequired => 31001,
1205            Self::BlindAuthFailed => 31002,
1206            Self::BatMintMaxExceeded => 31003,
1207            Self::BatRateLimitExceeded => 31004,
1208            Self::ConcurrentUpdate => 50000,
1209            Self::Unknown(code) => *code,
1210        }
1211    }
1212}
1213
1214impl Serialize for ErrorCode {
1215    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1216    where
1217        S: Serializer,
1218    {
1219        serializer.serialize_u16(self.to_code())
1220    }
1221}
1222
1223impl<'de> Deserialize<'de> for ErrorCode {
1224    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1225    where
1226        D: Deserializer<'de>,
1227    {
1228        let code = u16::deserialize(deserializer)?;
1229
1230        Ok(ErrorCode::from_code(code))
1231    }
1232}
1233
1234impl fmt::Display for ErrorCode {
1235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1236        write!(f, "{}", self.to_code())
1237    }
1238}