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