1use 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#[derive(Debug, Error)]
23pub enum Error {
24 #[error("No Key for Amount")]
26 AmountKey,
27 #[error("Keyset id not known: `{0}`")]
29 KeysetUnknown(Id),
30 #[error("Unit unsupported")]
32 UnsupportedUnit,
33 #[error("Payment failed")]
35 PaymentFailed,
36 #[error("Payment pending")]
38 PaymentPending,
39 #[error("Request already paid")]
41 RequestAlreadyPaid,
42 #[error("Invalid payment request")]
44 InvalidPaymentRequest,
45 #[error("Invoice Amount undefined")]
47 InvoiceAmountUndefined,
48 #[error("Split Values must be less then or equal to amount")]
50 SplitValuesGreater,
51 #[error("Amount Overflow")]
53 AmountOverflow,
54 #[error("Cannot issue more than amount paid")]
56 OverIssue,
57 #[error("Signature missing or invalid")]
59 SignatureMissingOrInvalid,
60 #[error("Amount Less Invoice is not allowed")]
62 AmountLessNotAllowed,
63 #[error("Multi-Part Internal Melt Quotes are not supported")]
65 InternalMultiPartMeltQuote,
66 #[error("Multi-Part payment is not supported for unit `{0}` and method `{1}`")]
68 MppUnitMethodNotSupported(CurrencyUnit, PaymentMethod),
69 #[error("Clear Auth Required")]
71 ClearAuthRequired,
72 #[error("Blind Auth Required")]
74 BlindAuthRequired,
75 #[error("Clear Auth Failed")]
77 ClearAuthFailed,
78 #[error("Blind Auth Failed")]
80 BlindAuthFailed,
81 #[error("Auth settings undefined")]
83 AuthSettingsUndefined,
84 #[error("Mint time outside of tolerance")]
86 MintTimeExceedsTolerance,
87 #[error("Insufficient blind auth tokens, must reauth")]
89 InsufficientBlindAuthTokens,
90 #[error("Auth localstore undefined")]
92 AuthLocalstoreUndefined,
93 #[error("Wallet cat not set")]
95 CatNotSet,
96 #[error("Could not get mint info")]
98 CouldNotGetMintInfo,
99 #[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")]
101 AmountlessInvoiceNotSupported(CurrencyUnit, PaymentMethod),
102 #[error("Payment id seen for mint")]
104 DuplicatePaymentId,
105 #[error("Pubkey required")]
107 PubkeyRequired,
108 #[error("Missing pubkey")]
110 MissingPubkey,
111 #[error("Invalid payment method")]
113 InvalidPaymentMethod,
114 #[error("Amount undefined")]
116 AmountUndefined,
117 #[error("Payment method unsupported")]
119 UnsupportedPaymentMethod,
120 #[error("Payment method required")]
122 PaymentMethodRequired,
123 #[error("Could not parse bolt12")]
125 Bolt12parse,
126 #[error("Could not parse invoice")]
128 InvalidInvoice,
129
130 #[error("Failed to parse BIP353 address: {0}")]
132 Bip353Parse(String),
133
134 #[error("Operation timeout")]
136 Timeout,
137 #[cfg(feature = "mint")]
147 #[error(
148 "Onchain backend returned request_lookup_id {got:?} that does not match \
149 mint-supplied quote_id {expected}"
150 )]
151 OnchainQuoteLookupIdMismatch {
152 expected: QuoteId,
154 got: Option<PaymentIdentifier>,
156 },
157
158 #[cfg(feature = "mint")]
166 #[error("Onchain melt quote must contain at least one fee_options entry")]
167 OnchainFeeOptionsEmpty,
168
169 #[cfg(feature = "mint")]
174 #[error("Duplicate fee_index {index} in onchain fee_options")]
175 OnchainFeeOptionsDuplicateIndex {
176 index: u32,
178 },
179
180 #[cfg(feature = "mint")]
183 #[error("Onchain melt request fee_index {index} not found in quote fee_options")]
184 OnchainFeeIndexNotFound {
185 index: u32,
187 },
188
189 #[error("Failed to resolve BIP353 address: {0}")]
191 Bip353Resolve(String),
192 #[error("No BOLT12 offer found in BIP353 payment instructions")]
194 Bip353NoBolt12Offer,
195
196 #[error("Failed to parse BIP321 payment instruction: {0}")]
198 Bip321Parse(String),
199 #[error("Failed to encode BIP321 payment request: {0}")]
201 Bip321Encode(String),
202
203 #[error("Failed to parse Lightning address: {0}")]
205 LightningAddressParse(String),
206 #[error("Failed to request invoice from Lightning address service: {0}")]
208 LightningAddressRequest(String),
209
210 #[error("Internal send error: {0}")]
212 SendError(String),
213
214 #[error("Internal receive error: {0}")]
216 RecvError(String),
217
218 #[error("Minting is disabled")]
221 MintingDisabled,
222 #[error("Unknown quote")]
224 UnknownQuote,
225 #[error("Expired quote: Expired: `{0}`, Time: `{1}`")]
227 ExpiredQuote(u64, u64),
228 #[error("Amount must be between `{0}` and `{1}` is `{2}`")]
230 AmountOutofLimitRange(Amount, Amount, Amount),
231 #[error("Quote not paid")]
233 UnpaidQuote,
234 #[error("Quote pending")]
236 PendingQuote,
237 #[error("Timed out waiting for pending melt to complete{}", .last_backend_error.as_ref().map(|e| format!(": last backend error: {}", e)).unwrap_or_default())]
243 PendingMeltTimeout {
244 last_backend_error: Option<String>,
246 },
247 #[error("Quote already issued")]
249 IssuedQuote,
250 #[error("Quote is already paid")]
252 PaidQuote,
253 #[error("Payment state is unknown")]
255 UnknownPaymentState,
256 #[error("Melting is disabled")]
258 MeltingDisabled,
259 #[error("Unknown Keyset")]
261 UnknownKeySet,
262 #[error("Blinded Message is already signed")]
264 BlindedMessageAlreadySigned,
265 #[error("Inactive Keyset")]
267 InactiveKeyset,
268 #[error("Keyset has expired")]
270 ExpiredKeyset,
271 #[error("Inputs: `{0}`, Outputs: `{1}`, Expected Fee: `{2}`")]
273 TransactionUnbalanced(u64, u64, u64),
274 #[error("Duplicate Inputs")]
276 DuplicateInputs,
277 #[error("Duplicate outputs")]
279 DuplicateOutputs,
280 #[error("Maximum inputs exceeded: {actual} provided, max {max}")]
282 MaxInputsExceeded {
283 actual: usize,
285 max: usize,
287 },
288 #[error("Maximum outputs exceeded: {actual} provided, max {max}")]
290 MaxOutputsExceeded {
291 actual: usize,
293 max: usize,
295 },
296 #[error("Duplicate quote IDs")]
298 DuplicateQuoteIds,
299 #[error("Maximum batch size exceeded: {actual} provided, max {max}")]
301 BatchSizeExceeded {
302 actual: usize,
304 max: usize,
306 },
307 #[error("Proof content too large: {actual} bytes, max {max}")]
309 ProofContentTooLarge {
310 actual: usize,
312 max: usize,
314 },
315 #[error("Request field '{field}' too large: {actual} bytes, max {max}")]
317 RequestFieldTooLarge {
318 field: String,
320 actual: usize,
322 max: usize,
324 },
325 #[error("Cannot have multiple units")]
327 MultipleUnits,
328 #[error("Input unit must match output")]
330 UnitMismatch,
331 #[error("Sig all cannot be used in melt")]
333 SigAllUsedInMelt,
334 #[error("Token Already Spent")]
336 TokenAlreadySpent,
337 #[error("Token Pending")]
339 TokenPending,
340 #[error("Internal Error")]
342 Internal,
343 #[error("Oidc client not set")]
345 OidcNotSet,
346 #[error("Unit string picked collided: `{0}`")]
348 UnitStringCollision(CurrencyUnit),
349 #[error("P2PK condition not met `{0}`")]
352 P2PKConditionsNotMet(String),
353 #[error("Duplicate signature from same pubkey in P2PK")]
355 DuplicateSignatureError,
356 #[error("Spending condition locktime not provided")]
358 LocktimeNotProvided,
359 #[error("Invalid spending conditions: `{0}`")]
361 InvalidSpendConditions(String),
362 #[error("Incorrect wallet: `{0}`")]
364 IncorrectWallet(String),
365 #[error("Unknown wallet: `{0}`")]
367 #[cfg(feature = "wallet")]
368 UnknownWallet(WalletKey),
369 #[error("Max fee exceeded")]
371 MaxFeeExceeded,
372 #[error("Invalid NUT-13 restore options: `{field}` {reason}")]
374 InvalidNut13Options {
375 field: &'static str,
377 reason: &'static str,
379 },
380 #[error("Url path segments could not be joined")]
382 UrlPathSegments,
383 #[error("Unknown error response: `{0}`")]
385 UnknownErrorResponse(String),
386 #[error("Could not verify DLEQ proof")]
388 CouldNotVerifyDleq,
389 #[error("Dleq proof not provided for signature")]
391 DleqProofNotProvided,
392 #[error("Token does not match wallet mint")]
395 IncorrectMint,
396 #[error("Multiple mint tokens not supported by receive. Please deconstruct the token and use receive with_proof")]
398 MultiMintTokenNotSupported,
399 #[error("Preimage not provided")]
401 PreimageNotProvided,
402
403 #[error("Unknown mint: {mint_url}")]
405 UnknownMint {
406 mint_url: String,
408 },
409 #[error("Transfer timeout: failed to transfer {amount} from {source_mint} to {target_mint}")]
411 TransferTimeout {
412 source_mint: String,
414 target_mint: String,
416 amount: Amount,
418 },
419 #[error("Insufficient funds")]
421 InsufficientFunds,
422 #[error("Unexpected proof state")]
424 UnexpectedProofState,
425 #[error("No active keyset")]
427 NoActiveKeyset,
428 #[error("Incorrect quote amount")]
430 IncorrectQuoteAmount,
431 #[error("Invoice Description not supported")]
433 InvoiceDescriptionUnsupported,
434 #[error("Invalid transaction direction")]
436 InvalidTransactionDirection,
437 #[error("Invalid transaction status")]
439 InvalidTransactionStatus,
440 #[error("Invalid transaction id")]
442 InvalidTransactionId,
443 #[error("Transaction not found")]
445 TransactionNotFound,
446 #[error("Invalid operation kind")]
448 InvalidOperationKind,
449 #[error("Invalid operation state")]
451 InvalidOperationState,
452 #[error("Operation not found")]
454 OperationNotFound,
455 #[error("Invalid KV store key or namespace: {0}")]
457 KVStoreInvalidKey(String),
458 #[error("Concurrent update detected")]
460 ConcurrentUpdate,
461 #[error("Invalid mint response: {0}")]
463 InvalidMintResponse(String),
464 #[error("Subscription error: {0}")]
466 SubscriptionError(String),
467 #[error("`{0}`")]
469 Custom(String),
470
471 #[error(transparent)]
474 Invoice(#[from] lightning_invoice::ParseOrSemanticError),
475 #[error(transparent)]
477 Bip32(#[from] bitcoin::bip32::Error),
478 #[error(transparent)]
480 ParseInt(#[from] std::num::ParseIntError),
481 #[error(transparent)]
483 UrlParseError(#[from] url::ParseError),
484 #[error(transparent)]
486 Utf8ParseError(#[from] std::string::FromUtf8Error),
487 #[error(transparent)]
489 SerdeJsonError(#[from] serde_json::Error),
490 #[error(transparent)]
492 Base64Error(#[from] bitcoin::base64::DecodeError),
493 #[error(transparent)]
495 HexError(#[from] hex::Error),
496 #[error("Http transport error {0:?}: {1}")]
498 HttpError(Option<u16>, String),
499 #[cfg(feature = "mint")]
501 #[error(transparent)]
502 Uuid(#[from] uuid::Error),
503 #[error(transparent)]
506 CashuUrl(#[from] crate::mint_url::Error),
507 #[error(transparent)]
509 Secret(#[from] crate::secret::Error),
510 #[error(transparent)]
512 AmountError(#[from] crate::amount::Error),
513 #[error(transparent)]
515 DHKE(#[from] crate::dhke::Error),
516 #[error(transparent)]
518 NUT00(#[from] crate::nuts::nut00::Error),
519 #[error(transparent)]
521 NUT01(#[from] crate::nuts::nut01::Error),
522 #[error(transparent)]
524 NUT02(#[from] crate::nuts::nut02::Error),
525 #[error(transparent)]
527 NUT03(#[from] crate::nuts::nut03::Error),
528 #[error(transparent)]
530 NUT04(#[from] crate::nuts::nut04::Error),
531 #[error(transparent)]
533 NUT05(#[from] crate::nuts::nut05::Error),
534 #[error(transparent)]
536 NUT10(crate::nuts::nut10::Error),
537 #[error(transparent)]
539 NUT11(#[from] crate::nuts::nut11::Error),
540 #[error(transparent)]
542 NUT12(#[from] crate::nuts::nut12::Error),
543 #[error(transparent)]
545 #[cfg(feature = "wallet")]
546 NUT13(#[from] crate::nuts::nut13::Error),
547 #[error(transparent)]
549 NUT14(#[from] crate::nuts::nut14::Error),
550 #[error(transparent)]
552 NUT18(#[from] crate::nuts::nut18::Error),
553 #[error(transparent)]
555 NUT20(#[from] crate::nuts::nut20::Error),
556 #[error(transparent)]
558 NUT21(#[from] crate::nuts::nut21::Error),
559 #[error(transparent)]
561 NUT22(#[from] crate::nuts::nut22::Error),
562 #[error(transparent)]
564 NUT23(#[from] crate::nuts::nut23::Error),
565 #[error(transparent)]
567 #[cfg(feature = "mint")]
568 QuoteId(#[from] crate::quote_id::QuoteIdError),
569 #[error(transparent)]
571 TryFromSliceError(#[from] TryFromSliceError),
572 #[error(transparent)]
574 Database(crate::database::Error),
575 #[error(transparent)]
577 #[cfg(feature = "mint")]
578 Payment(#[from] crate::payment::Error),
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584
585 #[test]
586 fn test_is_definitive_failure() {
587 assert!(Error::AmountOverflow.is_definitive_failure());
589 assert!(Error::MintingDisabled.is_definitive_failure());
590 assert!(Error::MaxInputsExceeded { actual: 2, max: 1 }.is_definitive_failure());
591 assert!(Error::MaxOutputsExceeded { actual: 2, max: 1 }.is_definitive_failure());
592
593 assert!(Error::HttpError(Some(400), "Bad Request".to_string()).is_definitive_failure());
595 assert!(Error::HttpError(Some(404), "Not Found".to_string()).is_definitive_failure());
596 assert!(
597 Error::HttpError(Some(429), "Too Many Requests".to_string()).is_definitive_failure()
598 );
599
600 assert!(!Error::Timeout.is_definitive_failure());
602 assert!(!Error::Internal.is_definitive_failure());
603 assert!(!Error::ConcurrentUpdate.is_definitive_failure());
604 assert!(!Error::BlindedMessageAlreadySigned.is_definitive_failure());
605 assert!(!Error::TokenAlreadySpent.is_definitive_failure());
606
607 assert!(
609 !Error::HttpError(Some(500), "Internal Server Error".to_string())
610 .is_definitive_failure()
611 );
612 assert!(!Error::HttpError(Some(502), "Bad Gateway".to_string()).is_definitive_failure());
613 assert!(
614 !Error::HttpError(Some(503), "Service Unavailable".to_string()).is_definitive_failure()
615 );
616
617 assert!(!Error::HttpError(None, "Connection refused".to_string()).is_definitive_failure());
619 }
620
621 #[test]
622 fn test_pending_states_are_ambiguous_failures() {
623 assert!(!Error::TokenPending.is_definitive_failure());
627 assert!(!Error::PendingQuote.is_definitive_failure());
628 }
629
630 #[test]
631 fn test_max_outputs_and_inputs_error_responses_decode() {
632 let max_inputs = Error::from(ErrorResponse {
633 code: ErrorCode::MaxInputsExceeded,
634 detail: "Maximum inputs exceeded: 2 provided, max 1".to_string(),
635 });
636 assert!(matches!(
637 max_inputs,
638 Error::MaxInputsExceeded { actual: 2, max: 1 }
639 ));
640 assert!(max_inputs.is_definitive_failure());
641
642 let max_outputs = Error::from(ErrorResponse {
643 code: ErrorCode::MaxOutputsExceeded,
644 detail: "Maximum outputs exceeded: 2 provided, max 1".to_string(),
645 });
646 assert!(matches!(
647 max_outputs,
648 Error::MaxOutputsExceeded { actual: 2, max: 1 }
649 ));
650 assert!(max_outputs.is_definitive_failure());
651 }
652
653 #[cfg(feature = "mint")]
654 #[test]
655 fn payment_backend_error_response_redacts_backend_detail() {
656 const BACKEND_DETAIL: &str = "backend secret: rpc-token-123";
657 let error = Error::Payment(crate::payment::Error::Custom(BACKEND_DETAIL.to_string()));
658
659 let response = ErrorResponse::from(error);
660
661 assert_eq!(response.code, ErrorCode::Unknown(50000));
662 assert_eq!(response.detail, "Payment backend error");
663 assert!(!response.detail.contains(BACKEND_DETAIL));
664 }
665}
666
667impl Error {
668 pub fn is_definitive_failure(&self) -> bool {
677 match self {
678 Self::AmountKey
680 | Self::KeysetUnknown(_)
681 | Self::UnsupportedUnit
682 | Self::InvoiceAmountUndefined
683 | Self::SplitValuesGreater
684 | Self::AmountOverflow
685 | Self::OverIssue
686 | Self::SignatureMissingOrInvalid
687 | Self::AmountLessNotAllowed
688 | Self::InternalMultiPartMeltQuote
689 | Self::MppUnitMethodNotSupported(_, _)
690 | Self::AmountlessInvoiceNotSupported(_, _)
691 | Self::DuplicatePaymentId
692 | Self::PubkeyRequired
693 | Self::InvalidPaymentMethod
694 | Self::UnsupportedPaymentMethod
695 | Self::InvalidInvoice
696 | Self::MintingDisabled
697 | Self::UnknownQuote
698 | Self::ExpiredQuote(_, _)
699 | Self::AmountOutofLimitRange(_, _, _)
700 | Self::UnpaidQuote
701 | Self::IssuedQuote
702 | Self::PaidQuote
703 | Self::MeltingDisabled
704 | Self::UnknownKeySet
705 | Self::InactiveKeyset
706 | Self::ExpiredKeyset
707 | Self::TransactionUnbalanced(_, _, _)
708 | Self::DuplicateInputs
709 | Self::DuplicateOutputs
710 | Self::MaxInputsExceeded { .. }
711 | Self::MaxOutputsExceeded { .. }
712 | Self::DuplicateQuoteIds
713 | Self::BatchSizeExceeded { .. }
714 | Self::MultipleUnits
715 | Self::UnitMismatch
716 | Self::SigAllUsedInMelt
717 | Self::P2PKConditionsNotMet(_)
718 | Self::DuplicateSignatureError
719 | Self::LocktimeNotProvided
720 | Self::InvalidSpendConditions(_)
721 | Self::IncorrectWallet(_)
722 | Self::MaxFeeExceeded
723 | Self::InvalidNut13Options { .. }
724 | Self::DleqProofNotProvided
725 | Self::IncorrectMint
726 | Self::MultiMintTokenNotSupported
727 | Self::PreimageNotProvided
728 | Self::UnknownMint { .. }
729 | Self::UnexpectedProofState
730 | Self::NoActiveKeyset
731 | Self::IncorrectQuoteAmount
732 | Self::InvoiceDescriptionUnsupported
733 | Self::InvalidTransactionDirection
734 | Self::InvalidTransactionStatus
735 | Self::InvalidTransactionId
736 | Self::InvalidOperationKind
737 | Self::InvalidOperationState
738 | Self::OperationNotFound
739 | Self::KVStoreInvalidKey(_)
740 | Self::Bip353Parse(_)
741 | Self::Bip353NoBolt12Offer
742 | Self::Bip321Parse(_)
743 | Self::Bip321Encode(_)
744 | Self::LightningAddressParse(_) => true,
745
746 #[cfg(feature = "mint")]
747 Self::OnchainQuoteLookupIdMismatch { .. }
748 | Self::OnchainFeeOptionsEmpty
749 | Self::OnchainFeeOptionsDuplicateIndex { .. }
750 | Self::OnchainFeeIndexNotFound { .. } => true,
751
752 Self::HttpError(Some(status), _) => {
754 (400..500).contains(status)
757 }
758
759 Self::Timeout
761 | Self::Internal
762 | Self::UnknownPaymentState
763 | Self::PendingQuote
764 | Self::TokenPending
765 | Self::CouldNotGetMintInfo
766 | Self::UnknownErrorResponse(_)
767 | Self::InvalidMintResponse(_)
768 | Self::ConcurrentUpdate
769 | Self::SendError(_)
770 | Self::RecvError(_)
771 | Self::TransferTimeout { .. }
772 | Self::Bip353Resolve(_)
773 | Self::LightningAddressRequest(_) => false,
774
775 Self::HttpError(None, _) | Self::SerdeJsonError(_) | Self::Database(_)
779 | Self::Custom(_) => false,
780
781 Self::ClearAuthRequired
783 | Self::BlindAuthRequired
784 | Self::ClearAuthFailed
785 | Self::BlindAuthFailed
786 | Self::InsufficientBlindAuthTokens
787 | Self::AuthSettingsUndefined
788 | Self::AuthLocalstoreUndefined
789 | Self::OidcNotSet => true,
790
791 Self::Invoice(_) => true, Self::Bip32(_) => true, Self::ParseInt(_) => true,
795 Self::UrlParseError(_) => true,
796 Self::Utf8ParseError(_) => true,
797 Self::Base64Error(_) => true,
798 Self::HexError(_) => true,
799 #[cfg(feature = "mint")]
800 Self::Uuid(_) => true,
801 Self::CashuUrl(_) => true,
802 Self::Secret(_) => true,
803 Self::AmountError(_) => true,
804 Self::DHKE(_) => true, Self::NUT00(_) => true,
806 Self::NUT01(_) => true,
807 Self::NUT02(_) => true,
808 Self::NUT03(_) => true,
809 Self::NUT04(_) => true,
810 Self::NUT05(_) => true,
811 Self::NUT11(_) => true,
812 Self::NUT12(_) => true,
813 #[cfg(feature = "wallet")]
814 Self::NUT13(_) => true,
815 Self::NUT14(_) => true,
816 Self::NUT18(_) => true,
817 Self::NUT20(_) => true,
818 Self::NUT21(_) => true,
819 Self::NUT22(_) => true,
820 Self::NUT23(_) => true,
821 #[cfg(feature = "mint")]
822 Self::QuoteId(_) => true,
823 Self::TryFromSliceError(_) => true,
824 #[cfg(feature = "mint")]
825 Self::Payment(_) => false, _ => false,
829 }
830 }
831}
832
833impl From<crate::nuts::nut10::Error> for Error {
834 fn from(err: crate::nuts::nut10::Error) -> Self {
835 match err {
836 crate::nuts::nut10::Error::NUT11(nut11_err) => Self::NUT11(nut11_err),
837 crate::nuts::nut10::Error::NUT14(nut14_err) => Self::NUT14(nut14_err),
838 other => Self::NUT10(other),
839 }
840 }
841}
842
843#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
847pub struct ErrorResponse {
848 pub code: ErrorCode,
850 #[serde(default)]
852 pub detail: String,
853}
854
855impl fmt::Display for ErrorResponse {
856 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
857 write!(f, "code: {}, detail: {}", self.code, self.detail)
858 }
859}
860
861impl ErrorResponse {
862 pub fn new(code: ErrorCode, detail: String) -> Self {
864 Self { code, detail }
865 }
866
867 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
869 let value: Value = serde_json::from_str(json)?;
870
871 Self::from_value(value)
872 }
873
874 pub fn from_value(value: Value) -> Result<Self, serde_json::Error> {
876 match serde_json::from_value::<ErrorResponse>(value.clone()) {
877 Ok(res) => Ok(res),
878 Err(_) => Ok(Self {
879 code: ErrorCode::Unknown(999),
880 detail: value.to_string(),
881 }),
882 }
883 }
884}
885
886fn map_nut11_error(_nut11_error: &crate::nuts::nut11::Error) -> ErrorCode {
889 ErrorCode::WitnessMissingOrInvalid
891}
892
893impl From<Error> for ErrorResponse {
894 fn from(err: Error) -> ErrorResponse {
895 match err {
896 Error::TokenAlreadySpent => ErrorResponse {
897 code: ErrorCode::TokenAlreadySpent,
898 detail: err.to_string(),
899 },
900 Error::UnsupportedUnit => ErrorResponse {
901 code: ErrorCode::UnsupportedUnit,
902 detail: err.to_string(),
903 },
904 Error::PaymentFailed => ErrorResponse {
905 code: ErrorCode::LightningError,
906 detail: err.to_string(),
907 },
908 Error::RequestAlreadyPaid => ErrorResponse {
909 code: ErrorCode::InvoiceAlreadyPaid,
910 detail: "Invoice already paid.".to_string(),
911 },
912 Error::TransactionUnbalanced(inputs_total, outputs_total, fee_expected) => {
913 ErrorResponse {
914 code: ErrorCode::TransactionUnbalanced,
915 detail: format!(
916 "Inputs: {inputs_total}, Outputs: {outputs_total}, expected_fee: {fee_expected}. Transaction inputs should equal outputs less fee"
917 ),
918 }
919 }
920 Error::MintingDisabled => ErrorResponse {
921 code: ErrorCode::MintingDisabled,
922 detail: err.to_string(),
923 },
924 Error::BlindedMessageAlreadySigned => ErrorResponse {
925 code: ErrorCode::BlindedMessageAlreadySigned,
926 detail: err.to_string(),
927 },
928 Error::InsufficientFunds => ErrorResponse {
929 code: ErrorCode::TransactionUnbalanced,
930 detail: err.to_string(),
931 },
932 Error::AmountOutofLimitRange(_min, _max, _amount) => ErrorResponse {
933 code: ErrorCode::AmountOutofLimitRange,
934 detail: err.to_string(),
935 },
936 Error::ExpiredQuote(_, _) => ErrorResponse {
937 code: ErrorCode::QuoteExpired,
938 detail: err.to_string(),
939 },
940 Error::PendingQuote => ErrorResponse {
941 code: ErrorCode::QuotePending,
942 detail: err.to_string(),
943 },
944 Error::PendingMeltTimeout { .. } => ErrorResponse {
945 code: ErrorCode::QuotePending,
946 detail: err.to_string(),
947 },
948 Error::TokenPending => ErrorResponse {
949 code: ErrorCode::TokenPending,
950 detail: err.to_string(),
951 },
952 Error::ClearAuthRequired => ErrorResponse {
953 code: ErrorCode::ClearAuthRequired,
954 detail: Error::ClearAuthRequired.to_string(),
955 },
956 Error::ClearAuthFailed => ErrorResponse {
957 code: ErrorCode::ClearAuthFailed,
958 detail: Error::ClearAuthFailed.to_string(),
959 },
960 Error::BlindAuthRequired => ErrorResponse {
961 code: ErrorCode::BlindAuthRequired,
962 detail: Error::BlindAuthRequired.to_string(),
963 },
964 Error::BlindAuthFailed => ErrorResponse {
965 code: ErrorCode::BlindAuthFailed,
966 detail: Error::BlindAuthFailed.to_string(),
967 },
968 Error::NUT20(err) => ErrorResponse {
969 code: ErrorCode::WitnessMissingOrInvalid,
970 detail: err.to_string(),
971 },
972 Error::DuplicateInputs => ErrorResponse {
973 code: ErrorCode::DuplicateInputs,
974 detail: err.to_string(),
975 },
976 Error::DuplicateOutputs => ErrorResponse {
977 code: ErrorCode::DuplicateOutputs,
978 detail: err.to_string(),
979 },
980 Error::MultipleUnits => ErrorResponse {
981 code: ErrorCode::MultipleUnits,
982 detail: err.to_string(),
983 },
984 Error::UnitMismatch => ErrorResponse {
985 code: ErrorCode::UnitMismatch,
986 detail: err.to_string(),
987 },
988 Error::UnpaidQuote => ErrorResponse {
989 code: ErrorCode::QuoteNotPaid,
990 detail: Error::UnpaidQuote.to_string(),
991 },
992 Error::NUT11(err) => {
993 let code = map_nut11_error(&err);
994 let extra = if matches!(err, crate::nuts::nut11::Error::SignaturesNotProvided) {
995 Some("P2PK signatures are required but not provided".to_string())
996 } else {
997 None
998 };
999 ErrorResponse {
1000 code,
1001 detail: match extra {
1002 Some(extra) => format!("{err}. {extra}"),
1003 None => err.to_string(),
1004 },
1005 }
1006 },
1007 Error::DuplicateSignatureError => ErrorResponse {
1008 code: ErrorCode::WitnessMissingOrInvalid,
1009 detail: err.to_string(),
1010 },
1011 Error::IssuedQuote => ErrorResponse {
1012 code: ErrorCode::TokensAlreadyIssued,
1013 detail: err.to_string(),
1014 },
1015 Error::UnknownKeySet => ErrorResponse {
1016 code: ErrorCode::KeysetNotFound,
1017 detail: err.to_string(),
1018 },
1019 Error::InactiveKeyset => ErrorResponse {
1020 code: ErrorCode::KeysetInactive,
1021 detail: err.to_string(),
1022 },
1023 Error::ExpiredKeyset => ErrorResponse {
1024 code: ErrorCode::KeysetExpired,
1025 detail: err.to_string(),
1026 },
1027 Error::AmountLessNotAllowed => ErrorResponse {
1028 code: ErrorCode::AmountlessInvoiceNotSupported,
1029 detail: err.to_string(),
1030 },
1031 Error::IncorrectQuoteAmount => ErrorResponse {
1032 code: ErrorCode::IncorrectQuoteAmount,
1033 detail: err.to_string(),
1034 },
1035 Error::PubkeyRequired => ErrorResponse {
1036 code: ErrorCode::PubkeyRequired,
1037 detail: err.to_string(),
1038 },
1039 Error::PaidQuote => ErrorResponse {
1040 code: ErrorCode::InvoiceAlreadyPaid,
1041 detail: err.to_string(),
1042 },
1043 Error::DuplicatePaymentId => ErrorResponse {
1044 code: ErrorCode::InvoiceAlreadyPaid,
1045 detail: err.to_string(),
1046 },
1047 Error::Database(crate::database::Error::Duplicate) => ErrorResponse {
1049 code: ErrorCode::InvoiceAlreadyPaid,
1050 detail: "Invoice already paid or pending".to_string(),
1051 },
1052
1053 Error::DHKE(crate::dhke::Error::TokenNotVerified) => ErrorResponse {
1055 code: ErrorCode::TokenNotVerified,
1056 detail: err.to_string(),
1057 },
1058 Error::DHKE(_) => ErrorResponse {
1059 code: ErrorCode::Unknown(50000),
1060 detail: err.to_string(),
1061 },
1062
1063 Error::CouldNotVerifyDleq => ErrorResponse {
1065 code: ErrorCode::TokenNotVerified,
1066 detail: err.to_string(),
1067 },
1068 Error::SignatureMissingOrInvalid => ErrorResponse {
1069 code: ErrorCode::WitnessMissingOrInvalid,
1070 detail: err.to_string(),
1071 },
1072 Error::SigAllUsedInMelt => ErrorResponse {
1073 code: ErrorCode::WitnessMissingOrInvalid,
1074 detail: err.to_string(),
1075 },
1076
1077 Error::AmountKey => ErrorResponse {
1079 code: ErrorCode::KeysetNotFound,
1080 detail: err.to_string(),
1081 },
1082 Error::KeysetUnknown(_) => ErrorResponse {
1083 code: ErrorCode::KeysetNotFound,
1084 detail: err.to_string(),
1085 },
1086 Error::NoActiveKeyset => ErrorResponse {
1087 code: ErrorCode::KeysetInactive,
1088 detail: err.to_string(),
1089 },
1090
1091 Error::UnknownQuote => ErrorResponse {
1093 code: ErrorCode::Unknown(50000),
1094 detail: err.to_string(),
1095 },
1096 Error::MeltingDisabled => ErrorResponse {
1097 code: ErrorCode::MintingDisabled,
1098 detail: err.to_string(),
1099 },
1100 Error::PaymentPending => ErrorResponse {
1101 code: ErrorCode::QuotePending,
1102 detail: err.to_string(),
1103 },
1104 Error::UnknownPaymentState => ErrorResponse {
1105 code: ErrorCode::Unknown(50000),
1106 detail: err.to_string(),
1107 },
1108 #[cfg(feature = "mint")]
1109 Error::Payment(payment_error) => {
1110 tracing::error!(error = %payment_error, "Payment backend error");
1111 ErrorResponse {
1112 code: ErrorCode::Unknown(50000),
1113 detail: "Payment backend error".to_string(),
1114 }
1115 }
1116
1117 Error::SplitValuesGreater => ErrorResponse {
1119 code: ErrorCode::TransactionUnbalanced,
1120 detail: err.to_string(),
1121 },
1122 Error::AmountOverflow => ErrorResponse {
1123 code: ErrorCode::TransactionUnbalanced,
1124 detail: err.to_string(),
1125 },
1126 Error::OverIssue => ErrorResponse {
1127 code: ErrorCode::TransactionUnbalanced,
1128 detail: err.to_string(),
1129 },
1130
1131 Error::InvalidPaymentRequest => ErrorResponse {
1133 code: ErrorCode::Unknown(50000),
1134 detail: err.to_string(),
1135 },
1136 Error::InvoiceAmountUndefined => ErrorResponse {
1137 code: ErrorCode::AmountlessInvoiceNotSupported,
1138 detail: err.to_string(),
1139 },
1140
1141 Error::Internal => ErrorResponse {
1143 code: ErrorCode::Unknown(50000),
1144 detail: err.to_string(),
1145 },
1146 Error::Database(_) => ErrorResponse {
1147 code: ErrorCode::Unknown(50000),
1148 detail: err.to_string(),
1149 },
1150 Error::ConcurrentUpdate => ErrorResponse {
1151 code: ErrorCode::ConcurrentUpdate,
1152 detail: err.to_string(),
1153 },
1154 Error::MaxInputsExceeded { .. } => ErrorResponse {
1155 code: ErrorCode::MaxInputsExceeded,
1156 detail: err.to_string()
1157 },
1158 Error::MaxOutputsExceeded { .. } => ErrorResponse {
1159 code: ErrorCode::MaxOutputsExceeded,
1160 detail: err.to_string()
1161 },
1162 Error::DuplicateQuoteIds => ErrorResponse {
1163 code: ErrorCode::DuplicateQuoteIds,
1164 detail: err.to_string(),
1165 },
1166 Error::BatchSizeExceeded { .. } => ErrorResponse {
1167 code: ErrorCode::BatchSizeExceeded,
1168 detail: err.to_string(),
1169 },
1170 _ => ErrorResponse {
1172 code: ErrorCode::Unknown(50000),
1173 detail: err.to_string(),
1174 },
1175 }
1176 }
1177}
1178
1179#[cfg(feature = "mint")]
1180impl From<crate::database::Error> for Error {
1181 fn from(db_error: crate::database::Error) -> Self {
1182 match db_error {
1183 crate::database::Error::InvalidStateTransition(state) => match state {
1184 crate::state::Error::Pending => Self::TokenPending,
1185 crate::state::Error::AlreadySpent => Self::TokenAlreadySpent,
1186 crate::state::Error::AlreadyPaid => Self::RequestAlreadyPaid,
1187 state => Self::Database(crate::database::Error::InvalidStateTransition(state)),
1188 },
1189 crate::database::Error::ConcurrentUpdate => Self::ConcurrentUpdate,
1190 db_error => Self::Database(db_error),
1191 }
1192 }
1193}
1194
1195#[cfg(not(feature = "mint"))]
1196impl From<crate::database::Error> for Error {
1197 fn from(db_error: crate::database::Error) -> Self {
1198 match db_error {
1199 crate::database::Error::ConcurrentUpdate => Self::ConcurrentUpdate,
1200 db_error => Self::Database(db_error),
1201 }
1202 }
1203}
1204
1205fn parse_limit_counts(detail: &str) -> Option<(usize, usize)> {
1206 let (_, counts) = detail.rsplit_once(": ")?;
1207 let (actual, max) = counts.split_once(" provided, max ")?;
1208
1209 Some((actual.trim().parse().ok()?, max.trim().parse().ok()?))
1210}
1211
1212impl From<ErrorResponse> for Error {
1213 fn from(err: ErrorResponse) -> Error {
1214 match err.code {
1215 ErrorCode::TokenNotVerified => Self::DHKE(crate::dhke::Error::TokenNotVerified),
1217 ErrorCode::TokenAlreadySpent => Self::TokenAlreadySpent,
1219 ErrorCode::TokenPending => Self::TokenPending,
1220 ErrorCode::BlindedMessageAlreadySigned => Self::BlindedMessageAlreadySigned,
1221 ErrorCode::OutputsPending => Self::TokenPending, ErrorCode::TransactionUnbalanced => Self::TransactionUnbalanced(0, 0, 0),
1223 ErrorCode::AmountOutofLimitRange => {
1224 Self::AmountOutofLimitRange(Amount::default(), Amount::default(), Amount::default())
1225 }
1226 ErrorCode::DuplicateInputs => Self::DuplicateInputs,
1227 ErrorCode::DuplicateOutputs => Self::DuplicateOutputs,
1228 ErrorCode::MaxInputsExceeded => {
1229 let (actual, max) = parse_limit_counts(&err.detail).unwrap_or((0, 0));
1230 Self::MaxInputsExceeded { actual, max }
1231 }
1232 ErrorCode::MaxOutputsExceeded => {
1233 let (actual, max) = parse_limit_counts(&err.detail).unwrap_or((0, 0));
1234 Self::MaxOutputsExceeded { actual, max }
1235 }
1236 ErrorCode::DuplicateQuoteIds => Self::DuplicateQuoteIds,
1237 ErrorCode::BatchSizeExceeded => Self::BatchSizeExceeded { actual: 0, max: 0 },
1238 ErrorCode::MultipleUnits => Self::MultipleUnits,
1239 ErrorCode::UnitMismatch => Self::UnitMismatch,
1240 ErrorCode::AmountlessInvoiceNotSupported => Self::AmountLessNotAllowed,
1241 ErrorCode::IncorrectQuoteAmount => Self::IncorrectQuoteAmount,
1242 ErrorCode::UnsupportedUnit => Self::UnsupportedUnit,
1243 ErrorCode::KeysetNotFound => Self::UnknownKeySet,
1245 ErrorCode::KeysetInactive => Self::InactiveKeyset,
1246 ErrorCode::KeysetExpired => Self::ExpiredKeyset,
1247 ErrorCode::QuoteNotPaid => Self::UnpaidQuote,
1249 ErrorCode::TokensAlreadyIssued => Self::IssuedQuote,
1250 ErrorCode::MintingDisabled => Self::MintingDisabled,
1251 ErrorCode::LightningError => Self::PaymentFailed,
1252 ErrorCode::QuotePending => Self::PendingQuote,
1253 ErrorCode::InvoiceAlreadyPaid => Self::RequestAlreadyPaid,
1254 ErrorCode::QuoteExpired => Self::ExpiredQuote(0, 0),
1255 ErrorCode::WitnessMissingOrInvalid => Self::SignatureMissingOrInvalid,
1256 ErrorCode::PubkeyRequired => Self::PubkeyRequired,
1257 ErrorCode::ClearAuthRequired => Self::ClearAuthRequired,
1259 ErrorCode::ClearAuthFailed => Self::ClearAuthFailed,
1260 ErrorCode::BlindAuthRequired => Self::BlindAuthRequired,
1262 ErrorCode::BlindAuthFailed => Self::BlindAuthFailed,
1263 ErrorCode::BatMintMaxExceeded => Self::InsufficientBlindAuthTokens,
1264 ErrorCode::BatRateLimitExceeded => Self::InsufficientBlindAuthTokens,
1265 _ => Self::UnknownErrorResponse(err.to_string()),
1266 }
1267 }
1268}
1269
1270#[cfg(feature = "http")]
1271impl From<crate::HttpError> for Error {
1272 fn from(err: crate::HttpError) -> Self {
1273 match &err {
1274 crate::HttpError::Status { status, message } => {
1275 Self::HttpError(Some(*status), message.clone())
1276 }
1277 _ => Self::HttpError(None, err.to_string()),
1278 }
1279 }
1280}
1281
1282#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
1284pub enum ErrorCode {
1285 TokenNotVerified,
1288
1289 TokenAlreadySpent,
1292 TokenPending,
1294 BlindedMessageAlreadySigned,
1296 OutputsPending,
1298 TransactionUnbalanced,
1300 AmountOutofLimitRange,
1302 DuplicateInputs,
1304 DuplicateOutputs,
1306 MultipleUnits,
1308 UnitMismatch,
1310 AmountlessInvoiceNotSupported,
1312 IncorrectQuoteAmount,
1314 UnsupportedUnit,
1316 MaxInputsExceeded,
1318 MaxOutputsExceeded,
1320 DuplicateQuoteIds,
1322 BatchSizeExceeded,
1324 KeysetNotFound,
1327 KeysetInactive,
1329 KeysetExpired,
1331
1332 QuoteNotPaid,
1335 TokensAlreadyIssued,
1337 MintingDisabled,
1339 LightningError,
1341 QuotePending,
1343 InvoiceAlreadyPaid,
1345 QuoteExpired,
1347 WitnessMissingOrInvalid,
1349 PubkeyRequired,
1351
1352 ClearAuthRequired,
1355 ClearAuthFailed,
1357
1358 BlindAuthRequired,
1361 BlindAuthFailed,
1363 BatMintMaxExceeded,
1365 BatRateLimitExceeded,
1367
1368 ConcurrentUpdate,
1370
1371 Unknown(u16),
1373}
1374
1375impl ErrorCode {
1376 pub fn from_code(code: u16) -> Self {
1378 match code {
1379 10001 => Self::TokenNotVerified,
1381 11001 => Self::TokenAlreadySpent,
1383 11002 => Self::TokenPending,
1384 11003 => Self::BlindedMessageAlreadySigned,
1385 11004 => Self::OutputsPending,
1386 11005 => Self::TransactionUnbalanced,
1387 11006 => Self::AmountOutofLimitRange,
1388 11007 => Self::DuplicateInputs,
1389 11008 => Self::DuplicateOutputs,
1390 11009 => Self::MultipleUnits,
1391 11010 => Self::UnitMismatch,
1392 11011 => Self::AmountlessInvoiceNotSupported,
1393 11012 => Self::IncorrectQuoteAmount,
1394 11013 => Self::UnsupportedUnit,
1395 11014 => Self::MaxInputsExceeded,
1396 11015 => Self::MaxOutputsExceeded,
1397 11016 => Self::DuplicateQuoteIds,
1398 11017 => Self::BatchSizeExceeded,
1399 12001 => Self::KeysetNotFound,
1401 12002 => Self::KeysetInactive,
1402 12003 => Self::KeysetExpired,
1403 20001 => Self::QuoteNotPaid,
1405 20002 => Self::TokensAlreadyIssued,
1406 20003 => Self::MintingDisabled,
1407 20004 => Self::LightningError,
1408 20005 => Self::QuotePending,
1409 20006 => Self::InvoiceAlreadyPaid,
1410 20007 => Self::QuoteExpired,
1411 20008 => Self::WitnessMissingOrInvalid,
1412 20009 => Self::PubkeyRequired,
1413 30001 => Self::ClearAuthRequired,
1415 30002 => Self::ClearAuthFailed,
1416 31001 => Self::BlindAuthRequired,
1418 31002 => Self::BlindAuthFailed,
1419 31003 => Self::BatMintMaxExceeded,
1420 31004 => Self::BatRateLimitExceeded,
1421 _ => Self::Unknown(code),
1422 }
1423 }
1424
1425 pub fn to_code(&self) -> u16 {
1427 match self {
1428 Self::TokenNotVerified => 10001,
1430 Self::TokenAlreadySpent => 11001,
1432 Self::TokenPending => 11002,
1433 Self::BlindedMessageAlreadySigned => 11003,
1434 Self::OutputsPending => 11004,
1435 Self::TransactionUnbalanced => 11005,
1436 Self::AmountOutofLimitRange => 11006,
1437 Self::DuplicateInputs => 11007,
1438 Self::DuplicateOutputs => 11008,
1439 Self::MultipleUnits => 11009,
1440 Self::UnitMismatch => 11010,
1441 Self::AmountlessInvoiceNotSupported => 11011,
1442 Self::IncorrectQuoteAmount => 11012,
1443 Self::UnsupportedUnit => 11013,
1444 Self::MaxInputsExceeded => 11014,
1445 Self::MaxOutputsExceeded => 11015,
1446 Self::DuplicateQuoteIds => 11016,
1447 Self::BatchSizeExceeded => 11017,
1448 Self::KeysetNotFound => 12001,
1450 Self::KeysetInactive => 12002,
1451 Self::KeysetExpired => 12003,
1452 Self::QuoteNotPaid => 20001,
1454 Self::TokensAlreadyIssued => 20002,
1455 Self::MintingDisabled => 20003,
1456 Self::LightningError => 20004,
1457 Self::QuotePending => 20005,
1458 Self::InvoiceAlreadyPaid => 20006,
1459 Self::QuoteExpired => 20007,
1460 Self::WitnessMissingOrInvalid => 20008,
1461 Self::PubkeyRequired => 20009,
1462 Self::ClearAuthRequired => 30001,
1464 Self::ClearAuthFailed => 30002,
1465 Self::BlindAuthRequired => 31001,
1467 Self::BlindAuthFailed => 31002,
1468 Self::BatMintMaxExceeded => 31003,
1469 Self::BatRateLimitExceeded => 31004,
1470 Self::ConcurrentUpdate => 50000,
1471 Self::Unknown(code) => *code,
1472 }
1473 }
1474}
1475
1476impl Serialize for ErrorCode {
1477 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1478 where
1479 S: Serializer,
1480 {
1481 serializer.serialize_u16(self.to_code())
1482 }
1483}
1484
1485impl<'de> Deserialize<'de> for ErrorCode {
1486 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1487 where
1488 D: Deserializer<'de>,
1489 {
1490 let code = u16::deserialize(deserializer)?;
1491
1492 Ok(ErrorCode::from_code(code))
1493 }
1494}
1495
1496impl fmt::Display for ErrorCode {
1497 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1498 write!(f, "{}", self.to_code())
1499 }
1500}