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