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