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