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