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