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 LN 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 id
438    #[error("Invalid transaction id")]
439    InvalidTransactionId,
440    /// Transaction not found
441    #[error("Transaction not found")]
442    TransactionNotFound,
443    /// Invalid operation kind
444    #[error("Invalid operation kind")]
445    InvalidOperationKind,
446    /// Invalid operation state
447    #[error("Invalid operation state")]
448    InvalidOperationState,
449    /// Operation not found
450    #[error("Operation not found")]
451    OperationNotFound,
452    /// KV Store invalid key or namespace
453    #[error("Invalid KV store key or namespace: {0}")]
454    KVStoreInvalidKey(String),
455    /// Concurrent update detected
456    #[error("Concurrent update detected")]
457    ConcurrentUpdate,
458    /// Invalid response from mint
459    #[error("Invalid mint response: {0}")]
460    InvalidMintResponse(String),
461    /// Subscription error
462    #[error("Subscription error: {0}")]
463    SubscriptionError(String),
464    /// Custom Error
465    #[error("`{0}`")]
466    Custom(String),
467
468    // External Error conversions
469    /// Parse invoice error
470    #[error(transparent)]
471    Invoice(#[from] lightning_invoice::ParseOrSemanticError),
472    /// Bip32 error
473    #[error(transparent)]
474    Bip32(#[from] bitcoin::bip32::Error),
475    /// Parse int error
476    #[error(transparent)]
477    ParseInt(#[from] std::num::ParseIntError),
478    /// Parse 9rl Error
479    #[error(transparent)]
480    UrlParseError(#[from] url::ParseError),
481    /// Utf8 parse error
482    #[error(transparent)]
483    Utf8ParseError(#[from] std::string::FromUtf8Error),
484    /// Serde Json error
485    #[error(transparent)]
486    SerdeJsonError(#[from] serde_json::Error),
487    /// Base64 error
488    #[error(transparent)]
489    Base64Error(#[from] bitcoin::base64::DecodeError),
490    /// From hex error
491    #[error(transparent)]
492    HexError(#[from] hex::Error),
493    /// Http transport error
494    #[error("Http transport error {0:?}: {1}")]
495    HttpError(Option<u16>, String),
496    /// Parse invoice error
497    #[cfg(feature = "mint")]
498    #[error(transparent)]
499    Uuid(#[from] uuid::Error),
500    // Crate error conversions
501    /// Cashu Url Error
502    #[error(transparent)]
503    CashuUrl(#[from] crate::mint_url::Error),
504    /// Secret error
505    #[error(transparent)]
506    Secret(#[from] crate::secret::Error),
507    /// Amount Error
508    #[error(transparent)]
509    AmountError(#[from] crate::amount::Error),
510    /// DHKE Error
511    #[error(transparent)]
512    DHKE(#[from] crate::dhke::Error),
513    /// NUT00 Error
514    #[error(transparent)]
515    NUT00(#[from] crate::nuts::nut00::Error),
516    /// Nut01 error
517    #[error(transparent)]
518    NUT01(#[from] crate::nuts::nut01::Error),
519    /// NUT02 error
520    #[error(transparent)]
521    NUT02(#[from] crate::nuts::nut02::Error),
522    /// NUT03 error
523    #[error(transparent)]
524    NUT03(#[from] crate::nuts::nut03::Error),
525    /// NUT04 error
526    #[error(transparent)]
527    NUT04(#[from] crate::nuts::nut04::Error),
528    /// NUT05 error
529    #[error(transparent)]
530    NUT05(#[from] crate::nuts::nut05::Error),
531    /// NUT10 Error
532    #[error(transparent)]
533    NUT10(crate::nuts::nut10::Error),
534    /// NUT11 Error
535    #[error(transparent)]
536    NUT11(#[from] crate::nuts::nut11::Error),
537    /// NUT12 Error
538    #[error(transparent)]
539    NUT12(#[from] crate::nuts::nut12::Error),
540    /// NUT13 Error
541    #[error(transparent)]
542    #[cfg(feature = "wallet")]
543    NUT13(#[from] crate::nuts::nut13::Error),
544    /// NUT14 Error
545    #[error(transparent)]
546    NUT14(#[from] crate::nuts::nut14::Error),
547    /// NUT18 Error
548    #[error(transparent)]
549    NUT18(#[from] crate::nuts::nut18::Error),
550    /// NUT20 Error
551    #[error(transparent)]
552    NUT20(#[from] crate::nuts::nut20::Error),
553    /// NUT21 Error
554    #[error(transparent)]
555    NUT21(#[from] crate::nuts::nut21::Error),
556    /// NUT22 Error
557    #[error(transparent)]
558    NUT22(#[from] crate::nuts::nut22::Error),
559    /// NUT23 Error
560    #[error(transparent)]
561    NUT23(#[from] crate::nuts::nut23::Error),
562    /// Quote ID Error
563    #[error(transparent)]
564    #[cfg(feature = "mint")]
565    QuoteId(#[from] crate::quote_id::QuoteIdError),
566    /// From slice error
567    #[error(transparent)]
568    TryFromSliceError(#[from] TryFromSliceError),
569    /// Database Error
570    #[error(transparent)]
571    Database(crate::database::Error),
572    /// Payment Error
573    #[error(transparent)]
574    #[cfg(feature = "mint")]
575    Payment(#[from] crate::payment::Error),
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581
582    #[test]
583    fn test_is_definitive_failure() {
584        // Test definitive failures
585        assert!(Error::AmountOverflow.is_definitive_failure());
586        assert!(Error::TokenAlreadySpent.is_definitive_failure());
587        assert!(Error::MintingDisabled.is_definitive_failure());
588
589        // Test HTTP client errors (4xx) - simulated
590        assert!(Error::HttpError(Some(400), "Bad Request".to_string()).is_definitive_failure());
591        assert!(Error::HttpError(Some(404), "Not Found".to_string()).is_definitive_failure());
592        assert!(
593            Error::HttpError(Some(429), "Too Many Requests".to_string()).is_definitive_failure()
594        );
595
596        // Test ambiguous failures
597        assert!(!Error::Timeout.is_definitive_failure());
598        assert!(!Error::Internal.is_definitive_failure());
599        assert!(!Error::ConcurrentUpdate.is_definitive_failure());
600
601        // Test HTTP server errors (5xx)
602        assert!(
603            !Error::HttpError(Some(500), "Internal Server Error".to_string())
604                .is_definitive_failure()
605        );
606        assert!(!Error::HttpError(Some(502), "Bad Gateway".to_string()).is_definitive_failure());
607        assert!(
608            !Error::HttpError(Some(503), "Service Unavailable".to_string()).is_definitive_failure()
609        );
610
611        // Test HTTP network errors (no status)
612        assert!(!Error::HttpError(None, "Connection refused".to_string()).is_definitive_failure());
613    }
614
615    #[test]
616    fn test_pending_states_are_ambiguous_failures() {
617        // In-flight pending states are indeterminate: the mint may still
618        // settle the operation, so reverting reserved proofs to Unspent and
619        // reusing them risks a double-spend or loss of funds.
620        assert!(!Error::TokenPending.is_definitive_failure());
621        assert!(!Error::PendingQuote.is_definitive_failure());
622    }
623}
624
625impl Error {
626    /// Check if the error is a definitive failure
627    ///
628    /// A definitive failure means the mint definitely rejected the request
629    /// and did not update its state. In these cases, it is safe to revert
630    /// the transaction locally.
631    ///
632    /// If false, the failure is ambiguous (e.g. timeout, network error, 500)
633    /// and the transaction state at the mint is unknown.
634    pub fn is_definitive_failure(&self) -> bool {
635        match self {
636            // Logic/Validation Errors (Safe to revert)
637            Self::AmountKey
638            | Self::KeysetUnknown(_)
639            | Self::UnsupportedUnit
640            | Self::InvoiceAmountUndefined
641            | Self::SplitValuesGreater
642            | Self::AmountOverflow
643            | Self::OverIssue
644            | Self::SignatureMissingOrInvalid
645            | Self::AmountLessNotAllowed
646            | Self::InternalMultiPartMeltQuote
647            | Self::MppUnitMethodNotSupported(_, _)
648            | Self::AmountlessInvoiceNotSupported(_, _)
649            | Self::DuplicatePaymentId
650            | Self::PubkeyRequired
651            | Self::InvalidPaymentMethod
652            | Self::UnsupportedPaymentMethod
653            | Self::InvalidInvoice
654            | Self::MintingDisabled
655            | Self::UnknownQuote
656            | Self::ExpiredQuote(_, _)
657            | Self::AmountOutofLimitRange(_, _, _)
658            | Self::UnpaidQuote
659            | Self::IssuedQuote
660            | Self::PaidQuote
661            | Self::MeltingDisabled
662            | Self::UnknownKeySet
663            | Self::BlindedMessageAlreadySigned
664            | Self::InactiveKeyset
665            | Self::ExpiredKeyset
666            | Self::TransactionUnbalanced(_, _, _)
667            | Self::DuplicateInputs
668            | Self::DuplicateOutputs
669            | Self::DuplicateQuoteIds
670            | Self::BatchSizeExceeded { .. }
671            | Self::MultipleUnits
672            | Self::UnitMismatch
673            | Self::SigAllUsedInMelt
674            | Self::TokenAlreadySpent
675            | Self::P2PKConditionsNotMet(_)
676            | Self::DuplicateSignatureError
677            | Self::LocktimeNotProvided
678            | Self::InvalidSpendConditions(_)
679            | Self::IncorrectWallet(_)
680            | Self::MaxFeeExceeded
681            | Self::InvalidNut13Options { .. }
682            | Self::DleqProofNotProvided
683            | Self::IncorrectMint
684            | Self::MultiMintTokenNotSupported
685            | Self::PreimageNotProvided
686            | Self::UnknownMint { .. }
687            | Self::UnexpectedProofState
688            | Self::NoActiveKeyset
689            | Self::IncorrectQuoteAmount
690            | Self::InvoiceDescriptionUnsupported
691            | Self::InvalidTransactionDirection
692            | Self::InvalidTransactionId
693            | Self::InvalidOperationKind
694            | Self::InvalidOperationState
695            | Self::OperationNotFound
696            | Self::KVStoreInvalidKey(_)
697            | Self::Bip353Parse(_)
698            | Self::Bip353NoBolt12Offer
699            | Self::Bip321Parse(_)
700            | Self::Bip321Encode(_)
701            | Self::LightningAddressParse(_) => true,
702
703            #[cfg(feature = "mint")]
704            Self::OnchainQuoteLookupIdMismatch { .. }
705            | Self::OnchainFeeOptionsEmpty
706            | Self::OnchainFeeOptionsDuplicateIndex { .. }
707            | Self::OnchainFeeIndexNotFound { .. } => true,
708
709            // HTTP Errors
710            Self::HttpError(Some(status), _) => {
711                // Client errors (400-499) are definitive failures
712                // Server errors (500-599) are ambiguous
713                (400..500).contains(status)
714            }
715
716            // Ambiguous Errors (Unsafe to revert)
717            Self::Timeout
718            | Self::Internal
719            | Self::UnknownPaymentState
720            | Self::PendingQuote
721            | Self::TokenPending
722            | Self::CouldNotGetMintInfo
723            | Self::UnknownErrorResponse(_)
724            | Self::InvalidMintResponse(_)
725            | Self::ConcurrentUpdate
726            | Self::SendError(_)
727            | Self::RecvError(_)
728            | Self::TransferTimeout { .. }
729            | Self::Bip353Resolve(_)
730            | Self::LightningAddressRequest(_) => false,
731
732            // Network/IO/Parsing Errors (Usually ambiguous as they could happen reading response)
733            Self::HttpError(None, _) // No status code means network error
734            | Self::SerdeJsonError(_) // Could be malformed success response
735            | Self::Database(_)
736            | Self::Custom(_) => false,
737
738            // Auth Errors (Generally definitive if rejected)
739            Self::ClearAuthRequired
740            | Self::BlindAuthRequired
741            | Self::ClearAuthFailed
742            | Self::BlindAuthFailed
743            | Self::InsufficientBlindAuthTokens
744            | Self::AuthSettingsUndefined
745            | Self::AuthLocalstoreUndefined
746            | Self::OidcNotSet => true,
747
748            // External conversions - check specifically
749            Self::Invoice(_) => true, // Parsing error
750            Self::Bip32(_) => true, // Key derivation error
751            Self::ParseInt(_) => true,
752            Self::UrlParseError(_) => true,
753            Self::Utf8ParseError(_) => true,
754            Self::Base64Error(_) => true,
755            Self::HexError(_) => true,
756            #[cfg(feature = "mint")]
757            Self::Uuid(_) => true,
758            Self::CashuUrl(_) => true,
759            Self::Secret(_) => true,
760            Self::AmountError(_) => true,
761            Self::DHKE(_) => true, // Crypto errors
762            Self::NUT00(_) => true,
763            Self::NUT01(_) => true,
764            Self::NUT02(_) => true,
765            Self::NUT03(_) => true,
766            Self::NUT04(_) => true,
767            Self::NUT05(_) => true,
768            Self::NUT11(_) => true,
769            Self::NUT12(_) => true,
770            #[cfg(feature = "wallet")]
771            Self::NUT13(_) => true,
772            Self::NUT14(_) => true,
773            Self::NUT18(_) => true,
774            Self::NUT20(_) => true,
775            Self::NUT21(_) => true,
776            Self::NUT22(_) => true,
777            Self::NUT23(_) => true,
778            #[cfg(feature = "mint")]
779            Self::QuoteId(_) => true,
780            Self::TryFromSliceError(_) => true,
781            #[cfg(feature = "mint")]
782            Self::Payment(_) => false, // Payment errors could be ambiguous? assume ambiguous to be safe
783
784            // Catch-all
785            _ => false,
786        }
787    }
788}
789
790impl From<crate::nuts::nut10::Error> for Error {
791    fn from(err: crate::nuts::nut10::Error) -> Self {
792        match err {
793            crate::nuts::nut10::Error::NUT11(nut11_err) => Self::NUT11(nut11_err),
794            crate::nuts::nut10::Error::NUT14(nut14_err) => Self::NUT14(nut14_err),
795            other => Self::NUT10(other),
796        }
797    }
798}
799
800/// CDK Error Response
801///
802/// See NUT definition in [00](https://github.com/cashubtc/nuts/blob/main/00.md)
803#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
804pub struct ErrorResponse {
805    /// Error Code
806    pub code: ErrorCode,
807    /// Human readable description
808    #[serde(default)]
809    pub detail: String,
810}
811
812impl fmt::Display for ErrorResponse {
813    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
814        write!(f, "code: {}, detail: {}", self.code, self.detail)
815    }
816}
817
818impl ErrorResponse {
819    /// Create new [`ErrorResponse`]
820    pub fn new(code: ErrorCode, detail: String) -> Self {
821        Self { code, detail }
822    }
823
824    /// Error response from json
825    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
826        let value: Value = serde_json::from_str(json)?;
827
828        Self::from_value(value)
829    }
830
831    /// Error response from json Value
832    pub fn from_value(value: Value) -> Result<Self, serde_json::Error> {
833        match serde_json::from_value::<ErrorResponse>(value.clone()) {
834            Ok(res) => Ok(res),
835            Err(_) => Ok(Self {
836                code: ErrorCode::Unknown(999),
837                detail: value.to_string(),
838            }),
839        }
840    }
841}
842
843/// Maps NUT11 errors to appropriate error codes
844/// All NUT11 errors are witness/signature related, so they map to WitnessMissingOrInvalid (20008)
845fn map_nut11_error(_nut11_error: &crate::nuts::nut11::Error) -> ErrorCode {
846    // All NUT11 errors relate to P2PK/witness validation, which maps to 20008
847    ErrorCode::WitnessMissingOrInvalid
848}
849
850impl From<Error> for ErrorResponse {
851    fn from(err: Error) -> ErrorResponse {
852        match err {
853            Error::TokenAlreadySpent => ErrorResponse {
854                code: ErrorCode::TokenAlreadySpent,
855                detail: err.to_string(),
856            },
857            Error::UnsupportedUnit => ErrorResponse {
858                code: ErrorCode::UnsupportedUnit,
859                detail: err.to_string(),
860            },
861            Error::PaymentFailed => ErrorResponse {
862                code: ErrorCode::LightningError,
863                detail: err.to_string(),
864            },
865            Error::RequestAlreadyPaid => ErrorResponse {
866                code: ErrorCode::InvoiceAlreadyPaid,
867                detail: "Invoice already paid.".to_string(),
868            },
869            Error::TransactionUnbalanced(inputs_total, outputs_total, fee_expected) => {
870                ErrorResponse {
871                    code: ErrorCode::TransactionUnbalanced,
872                    detail: format!(
873                        "Inputs: {inputs_total}, Outputs: {outputs_total}, expected_fee: {fee_expected}. Transaction inputs should equal outputs less fee"
874                    ),
875                }
876            }
877            Error::MintingDisabled => ErrorResponse {
878                code: ErrorCode::MintingDisabled,
879                detail: err.to_string(),
880            },
881            Error::BlindedMessageAlreadySigned => ErrorResponse {
882                code: ErrorCode::BlindedMessageAlreadySigned,
883                detail: err.to_string(),
884            },
885            Error::InsufficientFunds => ErrorResponse {
886                code: ErrorCode::TransactionUnbalanced,
887                detail: err.to_string(),
888            },
889            Error::AmountOutofLimitRange(_min, _max, _amount) => ErrorResponse {
890                code: ErrorCode::AmountOutofLimitRange,
891                detail: err.to_string(),
892            },
893            Error::ExpiredQuote(_, _) => ErrorResponse {
894                code: ErrorCode::QuoteExpired,
895                detail: err.to_string(),
896            },
897            Error::PendingQuote => ErrorResponse {
898                code: ErrorCode::QuotePending,
899                detail: err.to_string(),
900            },
901            Error::PendingMeltTimeout { .. } => ErrorResponse {
902                code: ErrorCode::QuotePending,
903                detail: err.to_string(),
904            },
905            Error::TokenPending => ErrorResponse {
906                code: ErrorCode::TokenPending,
907                detail: err.to_string(),
908            },
909            Error::ClearAuthRequired => ErrorResponse {
910                code: ErrorCode::ClearAuthRequired,
911                detail: Error::ClearAuthRequired.to_string(),
912            },
913            Error::ClearAuthFailed => ErrorResponse {
914                code: ErrorCode::ClearAuthFailed,
915                detail: Error::ClearAuthFailed.to_string(),
916            },
917            Error::BlindAuthRequired => ErrorResponse {
918                code: ErrorCode::BlindAuthRequired,
919                detail: Error::BlindAuthRequired.to_string(),
920            },
921            Error::BlindAuthFailed => ErrorResponse {
922                code: ErrorCode::BlindAuthFailed,
923                detail: Error::BlindAuthFailed.to_string(),
924            },
925            Error::NUT20(err) => ErrorResponse {
926                code: ErrorCode::WitnessMissingOrInvalid,
927                detail: err.to_string(),
928            },
929            Error::DuplicateInputs => ErrorResponse {
930                code: ErrorCode::DuplicateInputs,
931                detail: err.to_string(),
932            },
933            Error::DuplicateOutputs => ErrorResponse {
934                code: ErrorCode::DuplicateOutputs,
935                detail: err.to_string(),
936            },
937            Error::MultipleUnits => ErrorResponse {
938                code: ErrorCode::MultipleUnits,
939                detail: err.to_string(),
940            },
941            Error::UnitMismatch => ErrorResponse {
942                code: ErrorCode::UnitMismatch,
943                detail: err.to_string(),
944            },
945            Error::UnpaidQuote => ErrorResponse {
946                code: ErrorCode::QuoteNotPaid,
947                detail: Error::UnpaidQuote.to_string(),
948            },
949            Error::NUT11(err) => {
950                let code = map_nut11_error(&err);
951                let extra = if matches!(err, crate::nuts::nut11::Error::SignaturesNotProvided) {
952                    Some("P2PK signatures are required but not provided".to_string())
953                } else {
954                    None
955                };
956                ErrorResponse {
957                    code,
958                    detail: match extra {
959                        Some(extra) => format!("{err}. {extra}"),
960                        None => err.to_string(),
961                    },
962                }
963            },
964            Error::DuplicateSignatureError => ErrorResponse {
965                code: ErrorCode::WitnessMissingOrInvalid,
966                detail: err.to_string(),
967            },
968            Error::IssuedQuote => ErrorResponse {
969                code: ErrorCode::TokensAlreadyIssued,
970                detail: err.to_string(),
971            },
972            Error::UnknownKeySet => ErrorResponse {
973                code: ErrorCode::KeysetNotFound,
974                detail: err.to_string(),
975            },
976            Error::InactiveKeyset => ErrorResponse {
977                code: ErrorCode::KeysetInactive,
978                detail: err.to_string(),
979            },
980            Error::ExpiredKeyset => ErrorResponse {
981                code: ErrorCode::KeysetExpired,
982                detail: err.to_string(),
983            },
984            Error::AmountLessNotAllowed => ErrorResponse {
985                code: ErrorCode::AmountlessInvoiceNotSupported,
986                detail: err.to_string(),
987            },
988            Error::IncorrectQuoteAmount => ErrorResponse {
989                code: ErrorCode::IncorrectQuoteAmount,
990                detail: err.to_string(),
991            },
992            Error::PubkeyRequired => ErrorResponse {
993                code: ErrorCode::PubkeyRequired,
994                detail: err.to_string(),
995            },
996            Error::PaidQuote => ErrorResponse {
997                code: ErrorCode::InvoiceAlreadyPaid,
998                detail: err.to_string(),
999            },
1000            Error::DuplicatePaymentId => ErrorResponse {
1001                code: ErrorCode::InvoiceAlreadyPaid,
1002                detail: err.to_string(),
1003            },
1004            // Database duplicate error indicates another quote with same invoice is already pending/paid
1005            Error::Database(crate::database::Error::Duplicate) => ErrorResponse {
1006                code: ErrorCode::InvoiceAlreadyPaid,
1007                detail: "Invoice already paid or pending".to_string(),
1008            },
1009
1010            // DHKE errors - TokenNotVerified for actual verification failures
1011            Error::DHKE(crate::dhke::Error::TokenNotVerified) => ErrorResponse {
1012                code: ErrorCode::TokenNotVerified,
1013                detail: err.to_string(),
1014            },
1015            Error::DHKE(_) => ErrorResponse {
1016                code: ErrorCode::Unknown(50000),
1017                detail: err.to_string(),
1018            },
1019
1020            // Verification errors
1021            Error::CouldNotVerifyDleq => ErrorResponse {
1022                code: ErrorCode::TokenNotVerified,
1023                detail: err.to_string(),
1024            },
1025            Error::SignatureMissingOrInvalid => ErrorResponse {
1026                code: ErrorCode::WitnessMissingOrInvalid,
1027                detail: err.to_string(),
1028            },
1029            Error::SigAllUsedInMelt => ErrorResponse {
1030                code: ErrorCode::WitnessMissingOrInvalid,
1031                detail: err.to_string(),
1032            },
1033
1034            // Keyset/key errors
1035            Error::AmountKey => ErrorResponse {
1036                code: ErrorCode::KeysetNotFound,
1037                detail: err.to_string(),
1038            },
1039            Error::KeysetUnknown(_) => ErrorResponse {
1040                code: ErrorCode::KeysetNotFound,
1041                detail: err.to_string(),
1042            },
1043            Error::NoActiveKeyset => ErrorResponse {
1044                code: ErrorCode::KeysetInactive,
1045                detail: err.to_string(),
1046            },
1047
1048            // Quote/payment errors
1049            Error::UnknownQuote => ErrorResponse {
1050                code: ErrorCode::Unknown(50000),
1051                detail: err.to_string(),
1052            },
1053            Error::MeltingDisabled => ErrorResponse {
1054                code: ErrorCode::MintingDisabled,
1055                detail: err.to_string(),
1056            },
1057            Error::PaymentPending => ErrorResponse {
1058                code: ErrorCode::QuotePending,
1059                detail: err.to_string(),
1060            },
1061            Error::UnknownPaymentState => ErrorResponse {
1062                code: ErrorCode::Unknown(50000),
1063                detail: err.to_string(),
1064            },
1065
1066            // Transaction/amount errors
1067            Error::SplitValuesGreater => ErrorResponse {
1068                code: ErrorCode::TransactionUnbalanced,
1069                detail: err.to_string(),
1070            },
1071            Error::AmountOverflow => ErrorResponse {
1072                code: ErrorCode::TransactionUnbalanced,
1073                detail: err.to_string(),
1074            },
1075            Error::OverIssue => ErrorResponse {
1076                code: ErrorCode::TransactionUnbalanced,
1077                detail: err.to_string(),
1078            },
1079
1080            // Invoice parsing errors - no spec code for invalid format
1081            Error::InvalidPaymentRequest => ErrorResponse {
1082                code: ErrorCode::Unknown(50000),
1083                detail: err.to_string(),
1084            },
1085            Error::InvoiceAmountUndefined => ErrorResponse {
1086                code: ErrorCode::AmountlessInvoiceNotSupported,
1087                detail: err.to_string(),
1088            },
1089
1090            // Internal/system errors - use Unknown(99999)
1091            Error::Internal => ErrorResponse {
1092                code: ErrorCode::Unknown(50000),
1093                detail: err.to_string(),
1094            },
1095            Error::Database(_) => ErrorResponse {
1096                code: ErrorCode::Unknown(50000),
1097                detail: err.to_string(),
1098            },
1099            Error::ConcurrentUpdate => ErrorResponse {
1100                code: ErrorCode::ConcurrentUpdate,
1101                detail: err.to_string(),
1102            },
1103            Error::MaxInputsExceeded { .. } => ErrorResponse {
1104                code: ErrorCode::MaxInputsExceeded,
1105                detail: err.to_string()
1106            },
1107            Error::MaxOutputsExceeded { .. } => ErrorResponse {
1108                code: ErrorCode::MaxOutputsExceeded,
1109                detail: err.to_string()
1110            },
1111            Error::DuplicateQuoteIds => ErrorResponse {
1112                code: ErrorCode::DuplicateQuoteIds,
1113                detail: err.to_string(),
1114            },
1115            Error::BatchSizeExceeded { .. } => ErrorResponse {
1116                code: ErrorCode::BatchSizeExceeded,
1117                detail: err.to_string(),
1118            },
1119            // Fallback for any remaining errors - use Unknown(99999) instead of TokenNotVerified
1120            _ => ErrorResponse {
1121                code: ErrorCode::Unknown(50000),
1122                detail: err.to_string(),
1123            },
1124        }
1125    }
1126}
1127
1128#[cfg(feature = "mint")]
1129impl From<crate::database::Error> for Error {
1130    fn from(db_error: crate::database::Error) -> Self {
1131        match db_error {
1132            crate::database::Error::InvalidStateTransition(state) => match state {
1133                crate::state::Error::Pending => Self::TokenPending,
1134                crate::state::Error::AlreadySpent => Self::TokenAlreadySpent,
1135                crate::state::Error::AlreadyPaid => Self::RequestAlreadyPaid,
1136                state => Self::Database(crate::database::Error::InvalidStateTransition(state)),
1137            },
1138            crate::database::Error::ConcurrentUpdate => Self::ConcurrentUpdate,
1139            db_error => Self::Database(db_error),
1140        }
1141    }
1142}
1143
1144#[cfg(not(feature = "mint"))]
1145impl From<crate::database::Error> for Error {
1146    fn from(db_error: crate::database::Error) -> Self {
1147        match db_error {
1148            crate::database::Error::ConcurrentUpdate => Self::ConcurrentUpdate,
1149            db_error => Self::Database(db_error),
1150        }
1151    }
1152}
1153
1154impl From<ErrorResponse> for Error {
1155    fn from(err: ErrorResponse) -> Error {
1156        match err.code {
1157            // 10xxx - Proof/Token verification errors
1158            ErrorCode::TokenNotVerified => Self::DHKE(crate::dhke::Error::TokenNotVerified),
1159            // 11xxx - Input/Output errors
1160            ErrorCode::TokenAlreadySpent => Self::TokenAlreadySpent,
1161            ErrorCode::TokenPending => Self::TokenPending,
1162            ErrorCode::BlindedMessageAlreadySigned => Self::BlindedMessageAlreadySigned,
1163            ErrorCode::OutputsPending => Self::TokenPending, // Map to closest equivalent
1164            ErrorCode::TransactionUnbalanced => Self::TransactionUnbalanced(0, 0, 0),
1165            ErrorCode::AmountOutofLimitRange => {
1166                Self::AmountOutofLimitRange(Amount::default(), Amount::default(), Amount::default())
1167            }
1168            ErrorCode::DuplicateInputs => Self::DuplicateInputs,
1169            ErrorCode::DuplicateOutputs => Self::DuplicateOutputs,
1170            ErrorCode::DuplicateQuoteIds => Self::DuplicateQuoteIds,
1171            ErrorCode::BatchSizeExceeded => Self::BatchSizeExceeded { actual: 0, max: 0 },
1172            ErrorCode::MultipleUnits => Self::MultipleUnits,
1173            ErrorCode::UnitMismatch => Self::UnitMismatch,
1174            ErrorCode::AmountlessInvoiceNotSupported => Self::AmountLessNotAllowed,
1175            ErrorCode::IncorrectQuoteAmount => Self::IncorrectQuoteAmount,
1176            ErrorCode::UnsupportedUnit => Self::UnsupportedUnit,
1177            // 12xxx - Keyset errors
1178            ErrorCode::KeysetNotFound => Self::UnknownKeySet,
1179            ErrorCode::KeysetInactive => Self::InactiveKeyset,
1180            ErrorCode::KeysetExpired => Self::ExpiredKeyset,
1181            // 20xxx - Quote/Payment errors
1182            ErrorCode::QuoteNotPaid => Self::UnpaidQuote,
1183            ErrorCode::TokensAlreadyIssued => Self::IssuedQuote,
1184            ErrorCode::MintingDisabled => Self::MintingDisabled,
1185            ErrorCode::LightningError => Self::PaymentFailed,
1186            ErrorCode::QuotePending => Self::PendingQuote,
1187            ErrorCode::InvoiceAlreadyPaid => Self::RequestAlreadyPaid,
1188            ErrorCode::QuoteExpired => Self::ExpiredQuote(0, 0),
1189            ErrorCode::WitnessMissingOrInvalid => Self::SignatureMissingOrInvalid,
1190            ErrorCode::PubkeyRequired => Self::PubkeyRequired,
1191            // 30xxx - Clear auth errors
1192            ErrorCode::ClearAuthRequired => Self::ClearAuthRequired,
1193            ErrorCode::ClearAuthFailed => Self::ClearAuthFailed,
1194            // 31xxx - Blind auth errors
1195            ErrorCode::BlindAuthRequired => Self::BlindAuthRequired,
1196            ErrorCode::BlindAuthFailed => Self::BlindAuthFailed,
1197            ErrorCode::BatMintMaxExceeded => Self::InsufficientBlindAuthTokens,
1198            ErrorCode::BatRateLimitExceeded => Self::InsufficientBlindAuthTokens,
1199            _ => Self::UnknownErrorResponse(err.to_string()),
1200        }
1201    }
1202}
1203
1204/// Possible Error Codes
1205#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
1206pub enum ErrorCode {
1207    // 10xxx - Proof/Token verification errors
1208    /// Proof verification failed (10001)
1209    TokenNotVerified,
1210
1211    // 11xxx - Input/Output errors
1212    /// Proofs already spent (11001)
1213    TokenAlreadySpent,
1214    /// Proofs are pending (11002)
1215    TokenPending,
1216    /// Outputs already signed (11003)
1217    BlindedMessageAlreadySigned,
1218    /// Outputs are pending (11004)
1219    OutputsPending,
1220    /// Transaction is not balanced (11005)
1221    TransactionUnbalanced,
1222    /// Amount outside of limit range (11006)
1223    AmountOutofLimitRange,
1224    /// Duplicate inputs provided (11007)
1225    DuplicateInputs,
1226    /// Duplicate outputs provided (11008)
1227    DuplicateOutputs,
1228    /// Inputs/Outputs of multiple units (11009)
1229    MultipleUnits,
1230    /// Inputs and outputs not of same unit (11010)
1231    UnitMismatch,
1232    /// Amountless invoice is not supported (11011)
1233    AmountlessInvoiceNotSupported,
1234    /// Amount in request does not equal invoice (11012)
1235    IncorrectQuoteAmount,
1236    /// Unit in request is not supported (11013)
1237    UnsupportedUnit,
1238    /// The max number of inputs is exceeded
1239    MaxInputsExceeded,
1240    /// The max number of outputs is exceeded
1241    MaxOutputsExceeded,
1242    /// Duplicate quote IDs provided in a batch (11016)
1243    DuplicateQuoteIds,
1244    /// Batch size exceeds mint limit (11017)
1245    BatchSizeExceeded,
1246    // 12xxx - Keyset errors
1247    /// Keyset is not known (12001)
1248    KeysetNotFound,
1249    /// Keyset is inactive, cannot sign messages (12002)
1250    KeysetInactive,
1251    /// Keyset expired (12003)
1252    KeysetExpired,
1253
1254    // 20xxx - Quote/Payment errors
1255    /// Quote request is not paid (20001)
1256    QuoteNotPaid,
1257    /// Quote has already been issued (20002)
1258    TokensAlreadyIssued,
1259    /// Minting is disabled (20003)
1260    MintingDisabled,
1261    /// Lightning payment failed (20004)
1262    LightningError,
1263    /// Quote is pending (20005)
1264    QuotePending,
1265    /// Invoice already paid (20006)
1266    InvoiceAlreadyPaid,
1267    /// Quote is expired (20007)
1268    QuoteExpired,
1269    /// Signature for mint request invalid (20008)
1270    WitnessMissingOrInvalid,
1271    /// Pubkey required for mint quote (20009)
1272    PubkeyRequired,
1273
1274    // 30xxx - Clear auth errors
1275    /// Endpoint requires clear auth (30001)
1276    ClearAuthRequired,
1277    /// Clear authentication failed (30002)
1278    ClearAuthFailed,
1279
1280    // 31xxx - Blind auth errors
1281    /// Endpoint requires blind auth (31001)
1282    BlindAuthRequired,
1283    /// Blind authentication failed (31002)
1284    BlindAuthFailed,
1285    /// Maximum BAT mint amount exceeded (31003)
1286    BatMintMaxExceeded,
1287    /// BAT mint rate limit exceeded (31004)
1288    BatRateLimitExceeded,
1289
1290    /// Concurrent update detected
1291    ConcurrentUpdate,
1292
1293    /// Unknown error code
1294    Unknown(u16),
1295}
1296
1297impl ErrorCode {
1298    /// Error code from u16
1299    pub fn from_code(code: u16) -> Self {
1300        match code {
1301            // 10xxx - Proof/Token verification errors
1302            10001 => Self::TokenNotVerified,
1303            // 11xxx - Input/Output errors
1304            11001 => Self::TokenAlreadySpent,
1305            11002 => Self::TokenPending,
1306            11003 => Self::BlindedMessageAlreadySigned,
1307            11004 => Self::OutputsPending,
1308            11005 => Self::TransactionUnbalanced,
1309            11006 => Self::AmountOutofLimitRange,
1310            11007 => Self::DuplicateInputs,
1311            11008 => Self::DuplicateOutputs,
1312            11009 => Self::MultipleUnits,
1313            11010 => Self::UnitMismatch,
1314            11011 => Self::AmountlessInvoiceNotSupported,
1315            11012 => Self::IncorrectQuoteAmount,
1316            11013 => Self::UnsupportedUnit,
1317            11014 => Self::MaxInputsExceeded,
1318            11015 => Self::MaxOutputsExceeded,
1319            11016 => Self::DuplicateQuoteIds,
1320            11017 => Self::BatchSizeExceeded,
1321            // 12xxx - Keyset errors
1322            12001 => Self::KeysetNotFound,
1323            12002 => Self::KeysetInactive,
1324            12003 => Self::KeysetExpired,
1325            // 20xxx - Quote/Payment errors
1326            20001 => Self::QuoteNotPaid,
1327            20002 => Self::TokensAlreadyIssued,
1328            20003 => Self::MintingDisabled,
1329            20004 => Self::LightningError,
1330            20005 => Self::QuotePending,
1331            20006 => Self::InvoiceAlreadyPaid,
1332            20007 => Self::QuoteExpired,
1333            20008 => Self::WitnessMissingOrInvalid,
1334            20009 => Self::PubkeyRequired,
1335            // 30xxx - Clear auth errors
1336            30001 => Self::ClearAuthRequired,
1337            30002 => Self::ClearAuthFailed,
1338            // 31xxx - Blind auth errors
1339            31001 => Self::BlindAuthRequired,
1340            31002 => Self::BlindAuthFailed,
1341            31003 => Self::BatMintMaxExceeded,
1342            31004 => Self::BatRateLimitExceeded,
1343            _ => Self::Unknown(code),
1344        }
1345    }
1346
1347    /// Error code to u16
1348    pub fn to_code(&self) -> u16 {
1349        match self {
1350            // 10xxx - Proof/Token verification errors
1351            Self::TokenNotVerified => 10001,
1352            // 11xxx - Input/Output errors
1353            Self::TokenAlreadySpent => 11001,
1354            Self::TokenPending => 11002,
1355            Self::BlindedMessageAlreadySigned => 11003,
1356            Self::OutputsPending => 11004,
1357            Self::TransactionUnbalanced => 11005,
1358            Self::AmountOutofLimitRange => 11006,
1359            Self::DuplicateInputs => 11007,
1360            Self::DuplicateOutputs => 11008,
1361            Self::MultipleUnits => 11009,
1362            Self::UnitMismatch => 11010,
1363            Self::AmountlessInvoiceNotSupported => 11011,
1364            Self::IncorrectQuoteAmount => 11012,
1365            Self::UnsupportedUnit => 11013,
1366            Self::MaxInputsExceeded => 11014,
1367            Self::MaxOutputsExceeded => 11015,
1368            Self::DuplicateQuoteIds => 11016,
1369            Self::BatchSizeExceeded => 11017,
1370            // 12xxx - Keyset errors
1371            Self::KeysetNotFound => 12001,
1372            Self::KeysetInactive => 12002,
1373            Self::KeysetExpired => 12003,
1374            // 20xxx - Quote/Payment errors
1375            Self::QuoteNotPaid => 20001,
1376            Self::TokensAlreadyIssued => 20002,
1377            Self::MintingDisabled => 20003,
1378            Self::LightningError => 20004,
1379            Self::QuotePending => 20005,
1380            Self::InvoiceAlreadyPaid => 20006,
1381            Self::QuoteExpired => 20007,
1382            Self::WitnessMissingOrInvalid => 20008,
1383            Self::PubkeyRequired => 20009,
1384            // 30xxx - Clear auth errors
1385            Self::ClearAuthRequired => 30001,
1386            Self::ClearAuthFailed => 30002,
1387            // 31xxx - Blind auth errors
1388            Self::BlindAuthRequired => 31001,
1389            Self::BlindAuthFailed => 31002,
1390            Self::BatMintMaxExceeded => 31003,
1391            Self::BatRateLimitExceeded => 31004,
1392            Self::ConcurrentUpdate => 50000,
1393            Self::Unknown(code) => *code,
1394        }
1395    }
1396}
1397
1398impl Serialize for ErrorCode {
1399    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1400    where
1401        S: Serializer,
1402    {
1403        serializer.serialize_u16(self.to_code())
1404    }
1405}
1406
1407impl<'de> Deserialize<'de> for ErrorCode {
1408    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1409    where
1410        D: Deserializer<'de>,
1411    {
1412        let code = u16::deserialize(deserializer)?;
1413
1414        Ok(ErrorCode::from_code(code))
1415    }
1416}
1417
1418impl fmt::Display for ErrorCode {
1419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1420        write!(f, "{}", self.to_code())
1421    }
1422}