Skip to main content

cdk_common/
error.rs

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