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 id")]
439 InvalidTransactionId,
440 #[error("Transaction not found")]
442 TransactionNotFound,
443 #[error("Invalid operation kind")]
445 InvalidOperationKind,
446 #[error("Invalid operation state")]
448 InvalidOperationState,
449 #[error("Operation not found")]
451 OperationNotFound,
452 #[error("Invalid KV store key or namespace: {0}")]
454 KVStoreInvalidKey(String),
455 #[error("Concurrent update detected")]
457 ConcurrentUpdate,
458 #[error("Invalid mint response: {0}")]
460 InvalidMintResponse(String),
461 #[error("Subscription error: {0}")]
463 SubscriptionError(String),
464 #[error("`{0}`")]
466 Custom(String),
467
468 #[error(transparent)]
471 Invoice(#[from] lightning_invoice::ParseOrSemanticError),
472 #[error(transparent)]
474 Bip32(#[from] bitcoin::bip32::Error),
475 #[error(transparent)]
477 ParseInt(#[from] std::num::ParseIntError),
478 #[error(transparent)]
480 UrlParseError(#[from] url::ParseError),
481 #[error(transparent)]
483 Utf8ParseError(#[from] std::string::FromUtf8Error),
484 #[error(transparent)]
486 SerdeJsonError(#[from] serde_json::Error),
487 #[error(transparent)]
489 Base64Error(#[from] bitcoin::base64::DecodeError),
490 #[error(transparent)]
492 HexError(#[from] hex::Error),
493 #[error("Http transport error {0:?}: {1}")]
495 HttpError(Option<u16>, String),
496 #[cfg(feature = "mint")]
498 #[error(transparent)]
499 Uuid(#[from] uuid::Error),
500 #[error(transparent)]
503 CashuUrl(#[from] crate::mint_url::Error),
504 #[error(transparent)]
506 Secret(#[from] crate::secret::Error),
507 #[error(transparent)]
509 AmountError(#[from] crate::amount::Error),
510 #[error(transparent)]
512 DHKE(#[from] crate::dhke::Error),
513 #[error(transparent)]
515 NUT00(#[from] crate::nuts::nut00::Error),
516 #[error(transparent)]
518 NUT01(#[from] crate::nuts::nut01::Error),
519 #[error(transparent)]
521 NUT02(#[from] crate::nuts::nut02::Error),
522 #[error(transparent)]
524 NUT03(#[from] crate::nuts::nut03::Error),
525 #[error(transparent)]
527 NUT04(#[from] crate::nuts::nut04::Error),
528 #[error(transparent)]
530 NUT05(#[from] crate::nuts::nut05::Error),
531 #[error(transparent)]
533 NUT10(crate::nuts::nut10::Error),
534 #[error(transparent)]
536 NUT11(#[from] crate::nuts::nut11::Error),
537 #[error(transparent)]
539 NUT12(#[from] crate::nuts::nut12::Error),
540 #[error(transparent)]
542 #[cfg(feature = "wallet")]
543 NUT13(#[from] crate::nuts::nut13::Error),
544 #[error(transparent)]
546 NUT14(#[from] crate::nuts::nut14::Error),
547 #[error(transparent)]
549 NUT18(#[from] crate::nuts::nut18::Error),
550 #[error(transparent)]
552 NUT20(#[from] crate::nuts::nut20::Error),
553 #[error(transparent)]
555 NUT21(#[from] crate::nuts::nut21::Error),
556 #[error(transparent)]
558 NUT22(#[from] crate::nuts::nut22::Error),
559 #[error(transparent)]
561 NUT23(#[from] crate::nuts::nut23::Error),
562 #[error(transparent)]
564 #[cfg(feature = "mint")]
565 QuoteId(#[from] crate::quote_id::QuoteIdError),
566 #[error(transparent)]
568 TryFromSliceError(#[from] TryFromSliceError),
569 #[error(transparent)]
571 Database(crate::database::Error),
572 #[error(transparent)]
574 #[cfg(feature = "mint")]
575 Payment(#[from] crate::payment::Error),
576}
577
578#[cfg(test)]
579mod tests {
580 use super::*;
581
582 #[test]
583 fn test_is_definitive_failure() {
584 assert!(Error::AmountOverflow.is_definitive_failure());
586 assert!(Error::TokenAlreadySpent.is_definitive_failure());
587 assert!(Error::MintingDisabled.is_definitive_failure());
588 assert!(Error::MaxInputsExceeded { actual: 2, max: 1 }.is_definitive_failure());
589 assert!(Error::MaxOutputsExceeded { actual: 2, max: 1 }.is_definitive_failure());
590
591 assert!(Error::HttpError(Some(400), "Bad Request".to_string()).is_definitive_failure());
593 assert!(Error::HttpError(Some(404), "Not Found".to_string()).is_definitive_failure());
594 assert!(
595 Error::HttpError(Some(429), "Too Many Requests".to_string()).is_definitive_failure()
596 );
597
598 assert!(!Error::Timeout.is_definitive_failure());
600 assert!(!Error::Internal.is_definitive_failure());
601 assert!(!Error::ConcurrentUpdate.is_definitive_failure());
602
603 assert!(
605 !Error::HttpError(Some(500), "Internal Server Error".to_string())
606 .is_definitive_failure()
607 );
608 assert!(!Error::HttpError(Some(502), "Bad Gateway".to_string()).is_definitive_failure());
609 assert!(
610 !Error::HttpError(Some(503), "Service Unavailable".to_string()).is_definitive_failure()
611 );
612
613 assert!(!Error::HttpError(None, "Connection refused".to_string()).is_definitive_failure());
615 }
616
617 #[test]
618 fn test_pending_states_are_ambiguous_failures() {
619 assert!(!Error::TokenPending.is_definitive_failure());
623 assert!(!Error::PendingQuote.is_definitive_failure());
624 }
625
626 #[test]
627 fn test_max_outputs_and_inputs_error_responses_decode() {
628 let max_inputs = Error::from(ErrorResponse {
629 code: ErrorCode::MaxInputsExceeded,
630 detail: "Maximum inputs exceeded: 2 provided, max 1".to_string(),
631 });
632 assert!(matches!(
633 max_inputs,
634 Error::MaxInputsExceeded { actual: 2, max: 1 }
635 ));
636 assert!(max_inputs.is_definitive_failure());
637
638 let max_outputs = Error::from(ErrorResponse {
639 code: ErrorCode::MaxOutputsExceeded,
640 detail: "Maximum outputs exceeded: 2 provided, max 1".to_string(),
641 });
642 assert!(matches!(
643 max_outputs,
644 Error::MaxOutputsExceeded { actual: 2, max: 1 }
645 ));
646 assert!(max_outputs.is_definitive_failure());
647 }
648
649 #[cfg(feature = "mint")]
650 #[test]
651 fn payment_backend_error_response_uses_generic_detail() {
652 const BACKEND_DETAIL: &str = "internal backend detail";
653 let error = Error::Payment(crate::payment::Error::Custom(BACKEND_DETAIL.to_string()));
654
655 let response = ErrorResponse::from(error);
656
657 assert_eq!(response.code, ErrorCode::Unknown(50000));
658 assert_eq!(response.detail, "Payment backend error");
659 assert!(!response.detail.contains(BACKEND_DETAIL));
660 }
661}
662
663impl Error {
664 pub fn is_definitive_failure(&self) -> bool {
673 match self {
674 Self::AmountKey
676 | Self::KeysetUnknown(_)
677 | Self::UnsupportedUnit
678 | Self::InvoiceAmountUndefined
679 | Self::SplitValuesGreater
680 | Self::AmountOverflow
681 | Self::OverIssue
682 | Self::SignatureMissingOrInvalid
683 | Self::AmountLessNotAllowed
684 | Self::InternalMultiPartMeltQuote
685 | Self::MppUnitMethodNotSupported(_, _)
686 | Self::AmountlessInvoiceNotSupported(_, _)
687 | Self::DuplicatePaymentId
688 | Self::PubkeyRequired
689 | Self::InvalidPaymentMethod
690 | Self::UnsupportedPaymentMethod
691 | Self::InvalidInvoice
692 | Self::MintingDisabled
693 | Self::UnknownQuote
694 | Self::ExpiredQuote(_, _)
695 | Self::AmountOutofLimitRange(_, _, _)
696 | Self::UnpaidQuote
697 | Self::IssuedQuote
698 | Self::PaidQuote
699 | Self::MeltingDisabled
700 | Self::UnknownKeySet
701 | Self::BlindedMessageAlreadySigned
702 | Self::InactiveKeyset
703 | Self::ExpiredKeyset
704 | Self::TransactionUnbalanced(_, _, _)
705 | Self::DuplicateInputs
706 | Self::DuplicateOutputs
707 | Self::MaxInputsExceeded { .. }
708 | Self::MaxOutputsExceeded { .. }
709 | Self::DuplicateQuoteIds
710 | Self::BatchSizeExceeded { .. }
711 | Self::MultipleUnits
712 | Self::UnitMismatch
713 | Self::SigAllUsedInMelt
714 | Self::TokenAlreadySpent
715 | Self::P2PKConditionsNotMet(_)
716 | Self::DuplicateSignatureError
717 | Self::LocktimeNotProvided
718 | Self::InvalidSpendConditions(_)
719 | Self::IncorrectWallet(_)
720 | Self::MaxFeeExceeded
721 | Self::InvalidNut13Options { .. }
722 | Self::DleqProofNotProvided
723 | Self::IncorrectMint
724 | Self::MultiMintTokenNotSupported
725 | Self::PreimageNotProvided
726 | Self::UnknownMint { .. }
727 | Self::UnexpectedProofState
728 | Self::NoActiveKeyset
729 | Self::IncorrectQuoteAmount
730 | Self::InvoiceDescriptionUnsupported
731 | Self::InvalidTransactionDirection
732 | Self::InvalidTransactionId
733 | Self::InvalidOperationKind
734 | Self::InvalidOperationState
735 | Self::OperationNotFound
736 | Self::KVStoreInvalidKey(_)
737 | Self::Bip353Parse(_)
738 | Self::Bip353NoBolt12Offer
739 | Self::Bip321Parse(_)
740 | Self::Bip321Encode(_)
741 | Self::LightningAddressParse(_) => true,
742
743 #[cfg(feature = "mint")]
744 Self::OnchainQuoteLookupIdMismatch { .. }
745 | Self::OnchainFeeOptionsEmpty
746 | Self::OnchainFeeOptionsDuplicateIndex { .. }
747 | Self::OnchainFeeIndexNotFound { .. } => true,
748
749 Self::HttpError(Some(status), _) => {
751 (400..500).contains(status)
754 }
755
756 Self::Timeout
758 | Self::Internal
759 | Self::UnknownPaymentState
760 | Self::PendingQuote
761 | Self::TokenPending
762 | Self::CouldNotGetMintInfo
763 | Self::UnknownErrorResponse(_)
764 | Self::InvalidMintResponse(_)
765 | Self::ConcurrentUpdate
766 | Self::SendError(_)
767 | Self::RecvError(_)
768 | Self::TransferTimeout { .. }
769 | Self::Bip353Resolve(_)
770 | Self::LightningAddressRequest(_) => false,
771
772 Self::HttpError(None, _) | Self::SerdeJsonError(_) | Self::Database(_)
776 | Self::Custom(_) => false,
777
778 Self::ClearAuthRequired
780 | Self::BlindAuthRequired
781 | Self::ClearAuthFailed
782 | Self::BlindAuthFailed
783 | Self::InsufficientBlindAuthTokens
784 | Self::AuthSettingsUndefined
785 | Self::AuthLocalstoreUndefined
786 | Self::OidcNotSet => true,
787
788 Self::Invoice(_) => true, Self::Bip32(_) => true, Self::ParseInt(_) => true,
792 Self::UrlParseError(_) => true,
793 Self::Utf8ParseError(_) => true,
794 Self::Base64Error(_) => true,
795 Self::HexError(_) => true,
796 #[cfg(feature = "mint")]
797 Self::Uuid(_) => true,
798 Self::CashuUrl(_) => true,
799 Self::Secret(_) => true,
800 Self::AmountError(_) => true,
801 Self::DHKE(_) => true, Self::NUT00(_) => true,
803 Self::NUT01(_) => true,
804 Self::NUT02(_) => true,
805 Self::NUT03(_) => true,
806 Self::NUT04(_) => true,
807 Self::NUT05(_) => true,
808 Self::NUT11(_) => true,
809 Self::NUT12(_) => true,
810 #[cfg(feature = "wallet")]
811 Self::NUT13(_) => true,
812 Self::NUT14(_) => true,
813 Self::NUT18(_) => true,
814 Self::NUT20(_) => true,
815 Self::NUT21(_) => true,
816 Self::NUT22(_) => true,
817 Self::NUT23(_) => true,
818 #[cfg(feature = "mint")]
819 Self::QuoteId(_) => true,
820 Self::TryFromSliceError(_) => true,
821 #[cfg(feature = "mint")]
822 Self::Payment(_) => false, _ => false,
826 }
827 }
828}
829
830impl From<crate::nuts::nut10::Error> for Error {
831 fn from(err: crate::nuts::nut10::Error) -> Self {
832 match err {
833 crate::nuts::nut10::Error::NUT11(nut11_err) => Self::NUT11(nut11_err),
834 crate::nuts::nut10::Error::NUT14(nut14_err) => Self::NUT14(nut14_err),
835 other => Self::NUT10(other),
836 }
837 }
838}
839
840#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
844pub struct ErrorResponse {
845 pub code: ErrorCode,
847 #[serde(default)]
849 pub detail: String,
850}
851
852impl fmt::Display for ErrorResponse {
853 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
854 write!(f, "code: {}, detail: {}", self.code, self.detail)
855 }
856}
857
858impl ErrorResponse {
859 pub fn new(code: ErrorCode, detail: String) -> Self {
861 Self { code, detail }
862 }
863
864 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
866 let value: Value = serde_json::from_str(json)?;
867
868 Self::from_value(value)
869 }
870
871 pub fn from_value(value: Value) -> Result<Self, serde_json::Error> {
873 match serde_json::from_value::<ErrorResponse>(value.clone()) {
874 Ok(res) => Ok(res),
875 Err(_) => Ok(Self {
876 code: ErrorCode::Unknown(999),
877 detail: value.to_string(),
878 }),
879 }
880 }
881}
882
883fn map_nut11_error(_nut11_error: &crate::nuts::nut11::Error) -> ErrorCode {
886 ErrorCode::WitnessMissingOrInvalid
888}
889
890impl From<Error> for ErrorResponse {
891 fn from(err: Error) -> ErrorResponse {
892 match err {
893 Error::TokenAlreadySpent => ErrorResponse {
894 code: ErrorCode::TokenAlreadySpent,
895 detail: err.to_string(),
896 },
897 Error::UnsupportedUnit => ErrorResponse {
898 code: ErrorCode::UnsupportedUnit,
899 detail: err.to_string(),
900 },
901 Error::PaymentFailed => ErrorResponse {
902 code: ErrorCode::LightningError,
903 detail: err.to_string(),
904 },
905 Error::RequestAlreadyPaid => ErrorResponse {
906 code: ErrorCode::InvoiceAlreadyPaid,
907 detail: "Invoice already paid.".to_string(),
908 },
909 Error::TransactionUnbalanced(inputs_total, outputs_total, fee_expected) => {
910 ErrorResponse {
911 code: ErrorCode::TransactionUnbalanced,
912 detail: format!(
913 "Inputs: {inputs_total}, Outputs: {outputs_total}, expected_fee: {fee_expected}. Transaction inputs should equal outputs less fee"
914 ),
915 }
916 }
917 Error::MintingDisabled => ErrorResponse {
918 code: ErrorCode::MintingDisabled,
919 detail: err.to_string(),
920 },
921 Error::BlindedMessageAlreadySigned => ErrorResponse {
922 code: ErrorCode::BlindedMessageAlreadySigned,
923 detail: err.to_string(),
924 },
925 Error::InsufficientFunds => ErrorResponse {
926 code: ErrorCode::TransactionUnbalanced,
927 detail: err.to_string(),
928 },
929 Error::AmountOutofLimitRange(_min, _max, _amount) => ErrorResponse {
930 code: ErrorCode::AmountOutofLimitRange,
931 detail: err.to_string(),
932 },
933 Error::ExpiredQuote(_, _) => ErrorResponse {
934 code: ErrorCode::QuoteExpired,
935 detail: err.to_string(),
936 },
937 Error::PendingQuote => ErrorResponse {
938 code: ErrorCode::QuotePending,
939 detail: err.to_string(),
940 },
941 Error::PendingMeltTimeout { .. } => ErrorResponse {
942 code: ErrorCode::QuotePending,
943 detail: err.to_string(),
944 },
945 Error::TokenPending => ErrorResponse {
946 code: ErrorCode::TokenPending,
947 detail: err.to_string(),
948 },
949 Error::ClearAuthRequired => ErrorResponse {
950 code: ErrorCode::ClearAuthRequired,
951 detail: Error::ClearAuthRequired.to_string(),
952 },
953 Error::ClearAuthFailed => ErrorResponse {
954 code: ErrorCode::ClearAuthFailed,
955 detail: Error::ClearAuthFailed.to_string(),
956 },
957 Error::BlindAuthRequired => ErrorResponse {
958 code: ErrorCode::BlindAuthRequired,
959 detail: Error::BlindAuthRequired.to_string(),
960 },
961 Error::BlindAuthFailed => ErrorResponse {
962 code: ErrorCode::BlindAuthFailed,
963 detail: Error::BlindAuthFailed.to_string(),
964 },
965 Error::NUT20(err) => ErrorResponse {
966 code: ErrorCode::WitnessMissingOrInvalid,
967 detail: err.to_string(),
968 },
969 Error::DuplicateInputs => ErrorResponse {
970 code: ErrorCode::DuplicateInputs,
971 detail: err.to_string(),
972 },
973 Error::DuplicateOutputs => ErrorResponse {
974 code: ErrorCode::DuplicateOutputs,
975 detail: err.to_string(),
976 },
977 Error::MultipleUnits => ErrorResponse {
978 code: ErrorCode::MultipleUnits,
979 detail: err.to_string(),
980 },
981 Error::UnitMismatch => ErrorResponse {
982 code: ErrorCode::UnitMismatch,
983 detail: err.to_string(),
984 },
985 Error::UnpaidQuote => ErrorResponse {
986 code: ErrorCode::QuoteNotPaid,
987 detail: Error::UnpaidQuote.to_string(),
988 },
989 Error::NUT11(err) => {
990 let code = map_nut11_error(&err);
991 let extra = if matches!(err, crate::nuts::nut11::Error::SignaturesNotProvided) {
992 Some("P2PK signatures are required but not provided".to_string())
993 } else {
994 None
995 };
996 ErrorResponse {
997 code,
998 detail: match extra {
999 Some(extra) => format!("{err}. {extra}"),
1000 None => err.to_string(),
1001 },
1002 }
1003 },
1004 Error::DuplicateSignatureError => ErrorResponse {
1005 code: ErrorCode::WitnessMissingOrInvalid,
1006 detail: err.to_string(),
1007 },
1008 Error::IssuedQuote => ErrorResponse {
1009 code: ErrorCode::TokensAlreadyIssued,
1010 detail: err.to_string(),
1011 },
1012 Error::UnknownKeySet => ErrorResponse {
1013 code: ErrorCode::KeysetNotFound,
1014 detail: err.to_string(),
1015 },
1016 Error::InactiveKeyset => ErrorResponse {
1017 code: ErrorCode::KeysetInactive,
1018 detail: err.to_string(),
1019 },
1020 Error::ExpiredKeyset => ErrorResponse {
1021 code: ErrorCode::KeysetExpired,
1022 detail: err.to_string(),
1023 },
1024 Error::AmountLessNotAllowed => ErrorResponse {
1025 code: ErrorCode::AmountlessInvoiceNotSupported,
1026 detail: err.to_string(),
1027 },
1028 Error::IncorrectQuoteAmount => ErrorResponse {
1029 code: ErrorCode::IncorrectQuoteAmount,
1030 detail: err.to_string(),
1031 },
1032 Error::PubkeyRequired => ErrorResponse {
1033 code: ErrorCode::PubkeyRequired,
1034 detail: err.to_string(),
1035 },
1036 Error::PaidQuote => ErrorResponse {
1037 code: ErrorCode::InvoiceAlreadyPaid,
1038 detail: err.to_string(),
1039 },
1040 Error::DuplicatePaymentId => ErrorResponse {
1041 code: ErrorCode::InvoiceAlreadyPaid,
1042 detail: err.to_string(),
1043 },
1044 Error::Database(crate::database::Error::Duplicate) => ErrorResponse {
1046 code: ErrorCode::InvoiceAlreadyPaid,
1047 detail: "Invoice already paid or pending".to_string(),
1048 },
1049
1050 Error::DHKE(crate::dhke::Error::TokenNotVerified) => ErrorResponse {
1052 code: ErrorCode::TokenNotVerified,
1053 detail: err.to_string(),
1054 },
1055 Error::DHKE(_) => ErrorResponse {
1056 code: ErrorCode::Unknown(50000),
1057 detail: err.to_string(),
1058 },
1059
1060 Error::CouldNotVerifyDleq => ErrorResponse {
1062 code: ErrorCode::TokenNotVerified,
1063 detail: err.to_string(),
1064 },
1065 Error::SignatureMissingOrInvalid => ErrorResponse {
1066 code: ErrorCode::WitnessMissingOrInvalid,
1067 detail: err.to_string(),
1068 },
1069 Error::SigAllUsedInMelt => ErrorResponse {
1070 code: ErrorCode::WitnessMissingOrInvalid,
1071 detail: err.to_string(),
1072 },
1073
1074 Error::AmountKey => ErrorResponse {
1076 code: ErrorCode::KeysetNotFound,
1077 detail: err.to_string(),
1078 },
1079 Error::KeysetUnknown(_) => ErrorResponse {
1080 code: ErrorCode::KeysetNotFound,
1081 detail: err.to_string(),
1082 },
1083 Error::NoActiveKeyset => ErrorResponse {
1084 code: ErrorCode::KeysetInactive,
1085 detail: err.to_string(),
1086 },
1087
1088 Error::UnknownQuote => ErrorResponse {
1090 code: ErrorCode::Unknown(50000),
1091 detail: err.to_string(),
1092 },
1093 Error::MeltingDisabled => ErrorResponse {
1094 code: ErrorCode::MintingDisabled,
1095 detail: err.to_string(),
1096 },
1097 Error::PaymentPending => ErrorResponse {
1098 code: ErrorCode::QuotePending,
1099 detail: err.to_string(),
1100 },
1101 Error::UnknownPaymentState => ErrorResponse {
1102 code: ErrorCode::Unknown(50000),
1103 detail: err.to_string(),
1104 },
1105 #[cfg(feature = "mint")]
1106 Error::Payment(payment_error) => {
1107 tracing::error!(error = %payment_error, "Payment backend error");
1108 ErrorResponse {
1109 code: ErrorCode::Unknown(50000),
1110 detail: "Payment backend error".to_string(),
1111 }
1112 }
1113
1114 Error::SplitValuesGreater => ErrorResponse {
1116 code: ErrorCode::TransactionUnbalanced,
1117 detail: err.to_string(),
1118 },
1119 Error::AmountOverflow => ErrorResponse {
1120 code: ErrorCode::TransactionUnbalanced,
1121 detail: err.to_string(),
1122 },
1123 Error::OverIssue => ErrorResponse {
1124 code: ErrorCode::TransactionUnbalanced,
1125 detail: err.to_string(),
1126 },
1127
1128 Error::InvalidPaymentRequest => ErrorResponse {
1130 code: ErrorCode::Unknown(50000),
1131 detail: err.to_string(),
1132 },
1133 Error::InvoiceAmountUndefined => ErrorResponse {
1134 code: ErrorCode::AmountlessInvoiceNotSupported,
1135 detail: err.to_string(),
1136 },
1137
1138 Error::Internal => ErrorResponse {
1140 code: ErrorCode::Unknown(50000),
1141 detail: err.to_string(),
1142 },
1143 Error::Database(_) => ErrorResponse {
1144 code: ErrorCode::Unknown(50000),
1145 detail: err.to_string(),
1146 },
1147 Error::ConcurrentUpdate => ErrorResponse {
1148 code: ErrorCode::ConcurrentUpdate,
1149 detail: err.to_string(),
1150 },
1151 Error::MaxInputsExceeded { .. } => ErrorResponse {
1152 code: ErrorCode::MaxInputsExceeded,
1153 detail: err.to_string()
1154 },
1155 Error::MaxOutputsExceeded { .. } => ErrorResponse {
1156 code: ErrorCode::MaxOutputsExceeded,
1157 detail: err.to_string()
1158 },
1159 Error::DuplicateQuoteIds => ErrorResponse {
1160 code: ErrorCode::DuplicateQuoteIds,
1161 detail: err.to_string(),
1162 },
1163 Error::BatchSizeExceeded { .. } => ErrorResponse {
1164 code: ErrorCode::BatchSizeExceeded,
1165 detail: err.to_string(),
1166 },
1167 _ => ErrorResponse {
1169 code: ErrorCode::Unknown(50000),
1170 detail: err.to_string(),
1171 },
1172 }
1173 }
1174}
1175
1176#[cfg(feature = "mint")]
1177impl From<crate::database::Error> for Error {
1178 fn from(db_error: crate::database::Error) -> Self {
1179 match db_error {
1180 crate::database::Error::InvalidStateTransition(state) => match state {
1181 crate::state::Error::Pending => Self::TokenPending,
1182 crate::state::Error::AlreadySpent => Self::TokenAlreadySpent,
1183 crate::state::Error::AlreadyPaid => Self::RequestAlreadyPaid,
1184 state => Self::Database(crate::database::Error::InvalidStateTransition(state)),
1185 },
1186 crate::database::Error::ConcurrentUpdate => Self::ConcurrentUpdate,
1187 db_error => Self::Database(db_error),
1188 }
1189 }
1190}
1191
1192#[cfg(not(feature = "mint"))]
1193impl From<crate::database::Error> for Error {
1194 fn from(db_error: crate::database::Error) -> Self {
1195 match db_error {
1196 crate::database::Error::ConcurrentUpdate => Self::ConcurrentUpdate,
1197 db_error => Self::Database(db_error),
1198 }
1199 }
1200}
1201
1202fn parse_limit_counts(detail: &str) -> Option<(usize, usize)> {
1203 let (_, counts) = detail.rsplit_once(": ")?;
1204 let (actual, max) = counts.split_once(" provided, max ")?;
1205
1206 Some((actual.trim().parse().ok()?, max.trim().parse().ok()?))
1207}
1208
1209impl From<ErrorResponse> for Error {
1210 fn from(err: ErrorResponse) -> Error {
1211 match err.code {
1212 ErrorCode::TokenNotVerified => Self::DHKE(crate::dhke::Error::TokenNotVerified),
1214 ErrorCode::TokenAlreadySpent => Self::TokenAlreadySpent,
1216 ErrorCode::TokenPending => Self::TokenPending,
1217 ErrorCode::BlindedMessageAlreadySigned => Self::BlindedMessageAlreadySigned,
1218 ErrorCode::OutputsPending => Self::TokenPending, ErrorCode::TransactionUnbalanced => Self::TransactionUnbalanced(0, 0, 0),
1220 ErrorCode::AmountOutofLimitRange => {
1221 Self::AmountOutofLimitRange(Amount::default(), Amount::default(), Amount::default())
1222 }
1223 ErrorCode::DuplicateInputs => Self::DuplicateInputs,
1224 ErrorCode::DuplicateOutputs => Self::DuplicateOutputs,
1225 ErrorCode::MaxInputsExceeded => {
1226 let (actual, max) = parse_limit_counts(&err.detail).unwrap_or((0, 0));
1227 Self::MaxInputsExceeded { actual, max }
1228 }
1229 ErrorCode::MaxOutputsExceeded => {
1230 let (actual, max) = parse_limit_counts(&err.detail).unwrap_or((0, 0));
1231 Self::MaxOutputsExceeded { actual, max }
1232 }
1233 ErrorCode::DuplicateQuoteIds => Self::DuplicateQuoteIds,
1234 ErrorCode::BatchSizeExceeded => Self::BatchSizeExceeded { actual: 0, max: 0 },
1235 ErrorCode::MultipleUnits => Self::MultipleUnits,
1236 ErrorCode::UnitMismatch => Self::UnitMismatch,
1237 ErrorCode::AmountlessInvoiceNotSupported => Self::AmountLessNotAllowed,
1238 ErrorCode::IncorrectQuoteAmount => Self::IncorrectQuoteAmount,
1239 ErrorCode::UnsupportedUnit => Self::UnsupportedUnit,
1240 ErrorCode::KeysetNotFound => Self::UnknownKeySet,
1242 ErrorCode::KeysetInactive => Self::InactiveKeyset,
1243 ErrorCode::KeysetExpired => Self::ExpiredKeyset,
1244 ErrorCode::QuoteNotPaid => Self::UnpaidQuote,
1246 ErrorCode::TokensAlreadyIssued => Self::IssuedQuote,
1247 ErrorCode::MintingDisabled => Self::MintingDisabled,
1248 ErrorCode::LightningError => Self::PaymentFailed,
1249 ErrorCode::QuotePending => Self::PendingQuote,
1250 ErrorCode::InvoiceAlreadyPaid => Self::RequestAlreadyPaid,
1251 ErrorCode::QuoteExpired => Self::ExpiredQuote(0, 0),
1252 ErrorCode::WitnessMissingOrInvalid => Self::SignatureMissingOrInvalid,
1253 ErrorCode::PubkeyRequired => Self::PubkeyRequired,
1254 ErrorCode::ClearAuthRequired => Self::ClearAuthRequired,
1256 ErrorCode::ClearAuthFailed => Self::ClearAuthFailed,
1257 ErrorCode::BlindAuthRequired => Self::BlindAuthRequired,
1259 ErrorCode::BlindAuthFailed => Self::BlindAuthFailed,
1260 ErrorCode::BatMintMaxExceeded => Self::InsufficientBlindAuthTokens,
1261 ErrorCode::BatRateLimitExceeded => Self::InsufficientBlindAuthTokens,
1262 _ => Self::UnknownErrorResponse(err.to_string()),
1263 }
1264 }
1265}
1266
1267#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
1269pub enum ErrorCode {
1270 TokenNotVerified,
1273
1274 TokenAlreadySpent,
1277 TokenPending,
1279 BlindedMessageAlreadySigned,
1281 OutputsPending,
1283 TransactionUnbalanced,
1285 AmountOutofLimitRange,
1287 DuplicateInputs,
1289 DuplicateOutputs,
1291 MultipleUnits,
1293 UnitMismatch,
1295 AmountlessInvoiceNotSupported,
1297 IncorrectQuoteAmount,
1299 UnsupportedUnit,
1301 MaxInputsExceeded,
1303 MaxOutputsExceeded,
1305 DuplicateQuoteIds,
1307 BatchSizeExceeded,
1309 KeysetNotFound,
1312 KeysetInactive,
1314 KeysetExpired,
1316
1317 QuoteNotPaid,
1320 TokensAlreadyIssued,
1322 MintingDisabled,
1324 LightningError,
1326 QuotePending,
1328 InvoiceAlreadyPaid,
1330 QuoteExpired,
1332 WitnessMissingOrInvalid,
1334 PubkeyRequired,
1336
1337 ClearAuthRequired,
1340 ClearAuthFailed,
1342
1343 BlindAuthRequired,
1346 BlindAuthFailed,
1348 BatMintMaxExceeded,
1350 BatRateLimitExceeded,
1352
1353 ConcurrentUpdate,
1355
1356 Unknown(u16),
1358}
1359
1360impl ErrorCode {
1361 pub fn from_code(code: u16) -> Self {
1363 match code {
1364 10001 => Self::TokenNotVerified,
1366 11001 => Self::TokenAlreadySpent,
1368 11002 => Self::TokenPending,
1369 11003 => Self::BlindedMessageAlreadySigned,
1370 11004 => Self::OutputsPending,
1371 11005 => Self::TransactionUnbalanced,
1372 11006 => Self::AmountOutofLimitRange,
1373 11007 => Self::DuplicateInputs,
1374 11008 => Self::DuplicateOutputs,
1375 11009 => Self::MultipleUnits,
1376 11010 => Self::UnitMismatch,
1377 11011 => Self::AmountlessInvoiceNotSupported,
1378 11012 => Self::IncorrectQuoteAmount,
1379 11013 => Self::UnsupportedUnit,
1380 11014 => Self::MaxInputsExceeded,
1381 11015 => Self::MaxOutputsExceeded,
1382 11016 => Self::DuplicateQuoteIds,
1383 11017 => Self::BatchSizeExceeded,
1384 12001 => Self::KeysetNotFound,
1386 12002 => Self::KeysetInactive,
1387 12003 => Self::KeysetExpired,
1388 20001 => Self::QuoteNotPaid,
1390 20002 => Self::TokensAlreadyIssued,
1391 20003 => Self::MintingDisabled,
1392 20004 => Self::LightningError,
1393 20005 => Self::QuotePending,
1394 20006 => Self::InvoiceAlreadyPaid,
1395 20007 => Self::QuoteExpired,
1396 20008 => Self::WitnessMissingOrInvalid,
1397 20009 => Self::PubkeyRequired,
1398 30001 => Self::ClearAuthRequired,
1400 30002 => Self::ClearAuthFailed,
1401 31001 => Self::BlindAuthRequired,
1403 31002 => Self::BlindAuthFailed,
1404 31003 => Self::BatMintMaxExceeded,
1405 31004 => Self::BatRateLimitExceeded,
1406 _ => Self::Unknown(code),
1407 }
1408 }
1409
1410 pub fn to_code(&self) -> u16 {
1412 match self {
1413 Self::TokenNotVerified => 10001,
1415 Self::TokenAlreadySpent => 11001,
1417 Self::TokenPending => 11002,
1418 Self::BlindedMessageAlreadySigned => 11003,
1419 Self::OutputsPending => 11004,
1420 Self::TransactionUnbalanced => 11005,
1421 Self::AmountOutofLimitRange => 11006,
1422 Self::DuplicateInputs => 11007,
1423 Self::DuplicateOutputs => 11008,
1424 Self::MultipleUnits => 11009,
1425 Self::UnitMismatch => 11010,
1426 Self::AmountlessInvoiceNotSupported => 11011,
1427 Self::IncorrectQuoteAmount => 11012,
1428 Self::UnsupportedUnit => 11013,
1429 Self::MaxInputsExceeded => 11014,
1430 Self::MaxOutputsExceeded => 11015,
1431 Self::DuplicateQuoteIds => 11016,
1432 Self::BatchSizeExceeded => 11017,
1433 Self::KeysetNotFound => 12001,
1435 Self::KeysetInactive => 12002,
1436 Self::KeysetExpired => 12003,
1437 Self::QuoteNotPaid => 20001,
1439 Self::TokensAlreadyIssued => 20002,
1440 Self::MintingDisabled => 20003,
1441 Self::LightningError => 20004,
1442 Self::QuotePending => 20005,
1443 Self::InvoiceAlreadyPaid => 20006,
1444 Self::QuoteExpired => 20007,
1445 Self::WitnessMissingOrInvalid => 20008,
1446 Self::PubkeyRequired => 20009,
1447 Self::ClearAuthRequired => 30001,
1449 Self::ClearAuthFailed => 30002,
1450 Self::BlindAuthRequired => 31001,
1452 Self::BlindAuthFailed => 31002,
1453 Self::BatMintMaxExceeded => 31003,
1454 Self::BatRateLimitExceeded => 31004,
1455 Self::ConcurrentUpdate => 50000,
1456 Self::Unknown(code) => *code,
1457 }
1458 }
1459}
1460
1461impl Serialize for ErrorCode {
1462 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1463 where
1464 S: Serializer,
1465 {
1466 serializer.serialize_u16(self.to_code())
1467 }
1468}
1469
1470impl<'de> Deserialize<'de> for ErrorCode {
1471 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1472 where
1473 D: Deserializer<'de>,
1474 {
1475 let code = u16::deserialize(deserializer)?;
1476
1477 Ok(ErrorCode::from_code(code))
1478 }
1479}
1480
1481impl fmt::Display for ErrorCode {
1482 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1483 write!(f, "{}", self.to_code())
1484 }
1485}