use std::array::TryFromSliceError;
use std::fmt;
use cashu::{CurrencyUnit, PaymentMethod};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use thiserror::Error;
use crate::nuts::Id;
use crate::util::hex;
#[cfg(feature = "wallet")]
use crate::wallet::WalletKey;
use crate::Amount;
#[derive(Debug, Error)]
pub enum Error {
#[error("No Key for Amount")]
AmountKey,
#[error("Keyset id not known: `{0}`")]
KeysetUnknown(Id),
#[error("Unit unsupported")]
UnsupportedUnit,
#[error("Payment failed")]
PaymentFailed,
#[error("Payment pending")]
PaymentPending,
#[error("Request already paid")]
RequestAlreadyPaid,
#[error("Invalid payment request")]
InvalidPaymentRequest,
#[error("Invoice Amount undefined")]
InvoiceAmountUndefined,
#[error("Split Values must be less then or equal to amount")]
SplitValuesGreater,
#[error("Amount Overflow")]
AmountOverflow,
#[error("Cannot issue more than amount paid")]
OverIssue,
#[error("Signature missing or invalid")]
SignatureMissingOrInvalid,
#[error("Amount Less Invoice is not allowed")]
AmountLessNotAllowed,
#[error("Multi-Part Internal Melt Quotes are not supported")]
InternalMultiPartMeltQuote,
#[error("Multi-Part payment is not supported for unit `{0}` and method `{1}`")]
MppUnitMethodNotSupported(CurrencyUnit, PaymentMethod),
#[error("Clear Auth Required")]
ClearAuthRequired,
#[error("Blind Auth Required")]
BlindAuthRequired,
#[error("Clear Auth Failed")]
ClearAuthFailed,
#[error("Blind Auth Failed")]
BlindAuthFailed,
#[error("Auth settings undefined")]
AuthSettingsUndefined,
#[error("Mint time outside of tolerance")]
MintTimeExceedsTolerance,
#[error("Insufficient blind auth tokens, must reauth")]
InsufficientBlindAuthTokens,
#[error("Auth localstore undefined")]
AuthLocalstoreUndefined,
#[error("Wallet cat not set")]
CatNotSet,
#[error("Could not get mint info")]
CouldNotGetMintInfo,
#[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")]
AmountlessInvoiceNotSupported(CurrencyUnit, PaymentMethod),
#[error("Payment id seen for mint")]
DuplicatePaymentId,
#[error("Pubkey required")]
PubkeyRequired,
#[error("Invalid payment method")]
InvalidPaymentMethod,
#[error("Amount undefined")]
AmountUndefined,
#[error("Payment method unsupported")]
UnsupportedPaymentMethod,
#[error("Payment method required")]
PaymentMethodRequired,
#[error("Could not parse bolt12")]
Bolt12parse,
#[error("Could not parse invoice")]
InvalidInvoice,
#[error("Failed to parse BIP353 address: {0}")]
Bip353Parse(String),
#[error("Operation timeout")]
Timeout,
#[error("Failed to resolve BIP353 address: {0}")]
Bip353Resolve(String),
#[error("No BOLT12 offer found in BIP353 payment instructions")]
Bip353NoBolt12Offer,
#[error("Failed to parse BIP321 payment instruction: {0}")]
Bip321Parse(String),
#[error("Failed to encode BIP321 payment request: {0}")]
Bip321Encode(String),
#[error("Failed to parse Lightning address: {0}")]
LightningAddressParse(String),
#[error("Failed to request invoice from Lightning address service: {0}")]
LightningAddressRequest(String),
#[error("Internal send error: {0}")]
SendError(String),
#[error("Internal receive error: {0}")]
RecvError(String),
#[error("Minting is disabled")]
MintingDisabled,
#[error("Unknown quote")]
UnknownQuote,
#[error("Expired quote: Expired: `{0}`, Time: `{1}`")]
ExpiredQuote(u64, u64),
#[error("Amount must be between `{0}` and `{1}` is `{2}`")]
AmountOutofLimitRange(Amount, Amount, Amount),
#[error("Quote not paid")]
UnpaidQuote,
#[error("Quote pending")]
PendingQuote,
#[error("Quote already issued")]
IssuedQuote,
#[error("Quote is already paid")]
PaidQuote,
#[error("Payment state is unknown")]
UnknownPaymentState,
#[error("Melting is disabled")]
MeltingDisabled,
#[error("Unknown Keyset")]
UnknownKeySet,
#[error("Blinded Message is already signed")]
BlindedMessageAlreadySigned,
#[error("Inactive Keyset")]
InactiveKeyset,
#[error("Inputs: `{0}`, Outputs: `{1}`, Expected Fee: `{2}`")]
TransactionUnbalanced(u64, u64, u64),
#[error("Duplicate Inputs")]
DuplicateInputs,
#[error("Duplicate outputs")]
DuplicateOutputs,
#[error("Maximum inputs exceeded: {actual} provided, max {max}")]
MaxInputsExceeded {
actual: usize,
max: usize,
},
#[error("Maximum outputs exceeded: {actual} provided, max {max}")]
MaxOutputsExceeded {
actual: usize,
max: usize,
},
#[error("Proof content too large: {actual} bytes, max {max}")]
ProofContentTooLarge {
actual: usize,
max: usize,
},
#[error("Request field '{field}' too large: {actual} bytes, max {max}")]
RequestFieldTooLarge {
field: String,
actual: usize,
max: usize,
},
#[error("Cannot have multiple units")]
MultipleUnits,
#[error("Input unit must match output")]
UnitMismatch,
#[error("Sig all cannot be used in melt")]
SigAllUsedInMelt,
#[error("Token Already Spent")]
TokenAlreadySpent,
#[error("Token Pending")]
TokenPending,
#[error("Internal Error")]
Internal,
#[error("Oidc client not set")]
OidcNotSet,
#[error("Unit string picked collided: `{0}`")]
UnitStringCollision(CurrencyUnit),
#[error("P2PK condition not met `{0}`")]
P2PKConditionsNotMet(String),
#[error("Duplicate signature from same pubkey in P2PK")]
DuplicateSignatureError,
#[error("Spending condition locktime not provided")]
LocktimeNotProvided,
#[error("Invalid spending conditions: `{0}`")]
InvalidSpendConditions(String),
#[error("Incorrect wallet: `{0}`")]
IncorrectWallet(String),
#[error("Unknown wallet: `{0}`")]
#[cfg(feature = "wallet")]
UnknownWallet(WalletKey),
#[error("Max fee exceeded")]
MaxFeeExceeded,
#[error("Url path segments could not be joined")]
UrlPathSegments,
#[error("Unknown error response: `{0}`")]
UnknownErrorResponse(String),
#[error("Could not verify DLEQ proof")]
CouldNotVerifyDleq,
#[error("Dleq proof not provided for signature")]
DleqProofNotProvided,
#[error("Token does not match wallet mint")]
IncorrectMint,
#[error("Multiple mint tokens not supported by receive. Please deconstruct the token and use receive with_proof")]
MultiMintTokenNotSupported,
#[error("Preimage not provided")]
PreimageNotProvided,
#[error("Unknown mint: {mint_url}")]
UnknownMint {
mint_url: String,
},
#[error("Transfer timeout: failed to transfer {amount} from {source_mint} to {target_mint}")]
TransferTimeout {
source_mint: String,
target_mint: String,
amount: Amount,
},
#[error("Insufficient funds")]
InsufficientFunds,
#[error("Unexpected proof state")]
UnexpectedProofState,
#[error("No active keyset")]
NoActiveKeyset,
#[error("Incorrect quote amount")]
IncorrectQuoteAmount,
#[error("Invoice Description not supported")]
InvoiceDescriptionUnsupported,
#[error("Invalid transaction direction")]
InvalidTransactionDirection,
#[error("Invalid transaction id")]
InvalidTransactionId,
#[error("Transaction not found")]
TransactionNotFound,
#[error("Invalid operation kind")]
InvalidOperationKind,
#[error("Invalid operation state")]
InvalidOperationState,
#[error("Operation not found")]
OperationNotFound,
#[error("Invalid KV store key or namespace: {0}")]
KVStoreInvalidKey(String),
#[error("Concurrent update detected")]
ConcurrentUpdate,
#[error("Invalid mint response: {0}")]
InvalidMintResponse(String),
#[error("Subscription error: {0}")]
SubscriptionError(String),
#[error("`{0}`")]
Custom(String),
#[error(transparent)]
Invoice(#[from] lightning_invoice::ParseOrSemanticError),
#[error(transparent)]
Bip32(#[from] bitcoin::bip32::Error),
#[error(transparent)]
ParseInt(#[from] std::num::ParseIntError),
#[error(transparent)]
UrlParseError(#[from] url::ParseError),
#[error(transparent)]
Utf8ParseError(#[from] std::string::FromUtf8Error),
#[error(transparent)]
SerdeJsonError(#[from] serde_json::Error),
#[error(transparent)]
Base64Error(#[from] bitcoin::base64::DecodeError),
#[error(transparent)]
HexError(#[from] hex::Error),
#[error("Http transport error {0:?}: {1}")]
HttpError(Option<u16>, String),
#[cfg(feature = "mint")]
#[error(transparent)]
Uuid(#[from] uuid::Error),
#[error(transparent)]
CashuUrl(#[from] crate::mint_url::Error),
#[error(transparent)]
Secret(#[from] crate::secret::Error),
#[error(transparent)]
AmountError(#[from] crate::amount::Error),
#[error(transparent)]
DHKE(#[from] crate::dhke::Error),
#[error(transparent)]
NUT00(#[from] crate::nuts::nut00::Error),
#[error(transparent)]
NUT01(#[from] crate::nuts::nut01::Error),
#[error(transparent)]
NUT02(#[from] crate::nuts::nut02::Error),
#[error(transparent)]
NUT03(#[from] crate::nuts::nut03::Error),
#[error(transparent)]
NUT04(#[from] crate::nuts::nut04::Error),
#[error(transparent)]
NUT05(#[from] crate::nuts::nut05::Error),
#[error(transparent)]
NUT10(crate::nuts::nut10::Error),
#[error(transparent)]
NUT11(#[from] crate::nuts::nut11::Error),
#[error(transparent)]
NUT12(#[from] crate::nuts::nut12::Error),
#[error(transparent)]
#[cfg(feature = "wallet")]
NUT13(#[from] crate::nuts::nut13::Error),
#[error(transparent)]
NUT14(#[from] crate::nuts::nut14::Error),
#[error(transparent)]
NUT18(#[from] crate::nuts::nut18::Error),
#[error(transparent)]
NUT20(#[from] crate::nuts::nut20::Error),
#[error(transparent)]
NUT21(#[from] crate::nuts::nut21::Error),
#[error(transparent)]
NUT22(#[from] crate::nuts::nut22::Error),
#[error(transparent)]
NUT23(#[from] crate::nuts::nut23::Error),
#[error(transparent)]
#[cfg(feature = "mint")]
QuoteId(#[from] crate::quote_id::QuoteIdError),
#[error(transparent)]
TryFromSliceError(#[from] TryFromSliceError),
#[error(transparent)]
Database(crate::database::Error),
#[error(transparent)]
#[cfg(feature = "mint")]
Payment(#[from] crate::payment::Error),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_definitive_failure() {
assert!(Error::AmountOverflow.is_definitive_failure());
assert!(Error::TokenAlreadySpent.is_definitive_failure());
assert!(Error::MintingDisabled.is_definitive_failure());
assert!(Error::HttpError(Some(400), "Bad Request".to_string()).is_definitive_failure());
assert!(Error::HttpError(Some(404), "Not Found".to_string()).is_definitive_failure());
assert!(
Error::HttpError(Some(429), "Too Many Requests".to_string()).is_definitive_failure()
);
assert!(!Error::Timeout.is_definitive_failure());
assert!(!Error::Internal.is_definitive_failure());
assert!(!Error::ConcurrentUpdate.is_definitive_failure());
assert!(
!Error::HttpError(Some(500), "Internal Server Error".to_string())
.is_definitive_failure()
);
assert!(!Error::HttpError(Some(502), "Bad Gateway".to_string()).is_definitive_failure());
assert!(
!Error::HttpError(Some(503), "Service Unavailable".to_string()).is_definitive_failure()
);
assert!(!Error::HttpError(None, "Connection refused".to_string()).is_definitive_failure());
}
}
impl Error {
pub fn is_definitive_failure(&self) -> bool {
match self {
Self::AmountKey
| Self::KeysetUnknown(_)
| Self::UnsupportedUnit
| Self::InvoiceAmountUndefined
| Self::SplitValuesGreater
| Self::AmountOverflow
| Self::OverIssue
| Self::SignatureMissingOrInvalid
| Self::AmountLessNotAllowed
| Self::InternalMultiPartMeltQuote
| Self::MppUnitMethodNotSupported(_, _)
| Self::AmountlessInvoiceNotSupported(_, _)
| Self::DuplicatePaymentId
| Self::PubkeyRequired
| Self::InvalidPaymentMethod
| Self::UnsupportedPaymentMethod
| Self::InvalidInvoice
| Self::MintingDisabled
| Self::UnknownQuote
| Self::ExpiredQuote(_, _)
| Self::AmountOutofLimitRange(_, _, _)
| Self::UnpaidQuote
| Self::PendingQuote
| Self::IssuedQuote
| Self::PaidQuote
| Self::MeltingDisabled
| Self::UnknownKeySet
| Self::BlindedMessageAlreadySigned
| Self::InactiveKeyset
| Self::TransactionUnbalanced(_, _, _)
| Self::DuplicateInputs
| Self::DuplicateOutputs
| Self::MultipleUnits
| Self::UnitMismatch
| Self::SigAllUsedInMelt
| Self::TokenAlreadySpent
| Self::TokenPending
| Self::P2PKConditionsNotMet(_)
| Self::DuplicateSignatureError
| Self::LocktimeNotProvided
| Self::InvalidSpendConditions(_)
| Self::IncorrectWallet(_)
| Self::MaxFeeExceeded
| Self::DleqProofNotProvided
| Self::IncorrectMint
| Self::MultiMintTokenNotSupported
| Self::PreimageNotProvided
| Self::UnknownMint { .. }
| Self::UnexpectedProofState
| Self::NoActiveKeyset
| Self::IncorrectQuoteAmount
| Self::InvoiceDescriptionUnsupported
| Self::InvalidTransactionDirection
| Self::InvalidTransactionId
| Self::InvalidOperationKind
| Self::InvalidOperationState
| Self::OperationNotFound
| Self::KVStoreInvalidKey(_)
| Self::Bip353Parse(_)
| Self::Bip353NoBolt12Offer
| Self::Bip321Parse(_)
| Self::Bip321Encode(_)
| Self::LightningAddressParse(_) => true,
Self::HttpError(Some(status), _) => {
(400..500).contains(status)
}
Self::Timeout
| Self::Internal
| Self::UnknownPaymentState
| Self::CouldNotGetMintInfo
| Self::UnknownErrorResponse(_)
| Self::InvalidMintResponse(_)
| Self::ConcurrentUpdate
| Self::SendError(_)
| Self::RecvError(_)
| Self::TransferTimeout { .. }
| Self::Bip353Resolve(_)
| Self::LightningAddressRequest(_) => false,
Self::HttpError(None, _) | Self::SerdeJsonError(_) | Self::Database(_)
| Self::Custom(_) => false,
Self::ClearAuthRequired
| Self::BlindAuthRequired
| Self::ClearAuthFailed
| Self::BlindAuthFailed
| Self::InsufficientBlindAuthTokens
| Self::AuthSettingsUndefined
| Self::AuthLocalstoreUndefined
| Self::OidcNotSet => true,
Self::Invoice(_) => true, Self::Bip32(_) => true, Self::ParseInt(_) => true,
Self::UrlParseError(_) => true,
Self::Utf8ParseError(_) => true,
Self::Base64Error(_) => true,
Self::HexError(_) => true,
#[cfg(feature = "mint")]
Self::Uuid(_) => true,
Self::CashuUrl(_) => true,
Self::Secret(_) => true,
Self::AmountError(_) => true,
Self::DHKE(_) => true, Self::NUT00(_) => true,
Self::NUT01(_) => true,
Self::NUT02(_) => true,
Self::NUT03(_) => true,
Self::NUT04(_) => true,
Self::NUT05(_) => true,
Self::NUT11(_) => true,
Self::NUT12(_) => true,
#[cfg(feature = "wallet")]
Self::NUT13(_) => true,
Self::NUT14(_) => true,
Self::NUT18(_) => true,
Self::NUT20(_) => true,
Self::NUT21(_) => true,
Self::NUT22(_) => true,
Self::NUT23(_) => true,
#[cfg(feature = "mint")]
Self::QuoteId(_) => true,
Self::TryFromSliceError(_) => true,
#[cfg(feature = "mint")]
Self::Payment(_) => false,
_ => false,
}
}
}
impl From<crate::nuts::nut10::Error> for Error {
fn from(err: crate::nuts::nut10::Error) -> Self {
match err {
crate::nuts::nut10::Error::NUT11(nut11_err) => Self::NUT11(nut11_err),
crate::nuts::nut10::Error::NUT14(nut14_err) => Self::NUT14(nut14_err),
other => Self::NUT10(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub struct ErrorResponse {
pub code: ErrorCode,
#[serde(default)]
pub detail: String,
}
impl fmt::Display for ErrorResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "code: {}, detail: {}", self.code, self.detail)
}
}
impl ErrorResponse {
pub fn new(code: ErrorCode, detail: String) -> Self {
Self { code, detail }
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
let value: Value = serde_json::from_str(json)?;
Self::from_value(value)
}
pub fn from_value(value: Value) -> Result<Self, serde_json::Error> {
match serde_json::from_value::<ErrorResponse>(value.clone()) {
Ok(res) => Ok(res),
Err(_) => Ok(Self {
code: ErrorCode::Unknown(999),
detail: value.to_string(),
}),
}
}
}
fn map_nut11_error(_nut11_error: &crate::nuts::nut11::Error) -> ErrorCode {
ErrorCode::WitnessMissingOrInvalid
}
impl From<Error> for ErrorResponse {
fn from(err: Error) -> ErrorResponse {
match err {
Error::TokenAlreadySpent => ErrorResponse {
code: ErrorCode::TokenAlreadySpent,
detail: err.to_string(),
},
Error::UnsupportedUnit => ErrorResponse {
code: ErrorCode::UnsupportedUnit,
detail: err.to_string(),
},
Error::PaymentFailed => ErrorResponse {
code: ErrorCode::LightningError,
detail: err.to_string(),
},
Error::RequestAlreadyPaid => ErrorResponse {
code: ErrorCode::InvoiceAlreadyPaid,
detail: "Invoice already paid.".to_string(),
},
Error::TransactionUnbalanced(inputs_total, outputs_total, fee_expected) => {
ErrorResponse {
code: ErrorCode::TransactionUnbalanced,
detail: format!(
"Inputs: {inputs_total}, Outputs: {outputs_total}, expected_fee: {fee_expected}. Transaction inputs should equal outputs less fee"
),
}
}
Error::MintingDisabled => ErrorResponse {
code: ErrorCode::MintingDisabled,
detail: err.to_string(),
},
Error::BlindedMessageAlreadySigned => ErrorResponse {
code: ErrorCode::BlindedMessageAlreadySigned,
detail: err.to_string(),
},
Error::InsufficientFunds => ErrorResponse {
code: ErrorCode::TransactionUnbalanced,
detail: err.to_string(),
},
Error::AmountOutofLimitRange(_min, _max, _amount) => ErrorResponse {
code: ErrorCode::AmountOutofLimitRange,
detail: err.to_string(),
},
Error::ExpiredQuote(_, _) => ErrorResponse {
code: ErrorCode::QuoteExpired,
detail: err.to_string(),
},
Error::PendingQuote => ErrorResponse {
code: ErrorCode::QuotePending,
detail: err.to_string(),
},
Error::TokenPending => ErrorResponse {
code: ErrorCode::TokenPending,
detail: err.to_string(),
},
Error::ClearAuthRequired => ErrorResponse {
code: ErrorCode::ClearAuthRequired,
detail: Error::ClearAuthRequired.to_string(),
},
Error::ClearAuthFailed => ErrorResponse {
code: ErrorCode::ClearAuthFailed,
detail: Error::ClearAuthFailed.to_string(),
},
Error::BlindAuthRequired => ErrorResponse {
code: ErrorCode::BlindAuthRequired,
detail: Error::BlindAuthRequired.to_string(),
},
Error::BlindAuthFailed => ErrorResponse {
code: ErrorCode::BlindAuthFailed,
detail: Error::BlindAuthFailed.to_string(),
},
Error::NUT20(err) => ErrorResponse {
code: ErrorCode::WitnessMissingOrInvalid,
detail: err.to_string(),
},
Error::DuplicateInputs => ErrorResponse {
code: ErrorCode::DuplicateInputs,
detail: err.to_string(),
},
Error::DuplicateOutputs => ErrorResponse {
code: ErrorCode::DuplicateOutputs,
detail: err.to_string(),
},
Error::MultipleUnits => ErrorResponse {
code: ErrorCode::MultipleUnits,
detail: err.to_string(),
},
Error::UnitMismatch => ErrorResponse {
code: ErrorCode::UnitMismatch,
detail: err.to_string(),
},
Error::UnpaidQuote => ErrorResponse {
code: ErrorCode::QuoteNotPaid,
detail: Error::UnpaidQuote.to_string(),
},
Error::NUT11(err) => {
let code = map_nut11_error(&err);
let extra = if matches!(err, crate::nuts::nut11::Error::SignaturesNotProvided) {
Some("P2PK signatures are required but not provided".to_string())
} else {
None
};
ErrorResponse {
code,
detail: match extra {
Some(extra) => format!("{err}. {extra}"),
None => err.to_string(),
},
}
},
Error::DuplicateSignatureError => ErrorResponse {
code: ErrorCode::WitnessMissingOrInvalid,
detail: err.to_string(),
},
Error::IssuedQuote => ErrorResponse {
code: ErrorCode::TokensAlreadyIssued,
detail: err.to_string(),
},
Error::UnknownKeySet => ErrorResponse {
code: ErrorCode::KeysetNotFound,
detail: err.to_string(),
},
Error::InactiveKeyset => ErrorResponse {
code: ErrorCode::KeysetInactive,
detail: err.to_string(),
},
Error::AmountLessNotAllowed => ErrorResponse {
code: ErrorCode::AmountlessInvoiceNotSupported,
detail: err.to_string(),
},
Error::IncorrectQuoteAmount => ErrorResponse {
code: ErrorCode::IncorrectQuoteAmount,
detail: err.to_string(),
},
Error::PubkeyRequired => ErrorResponse {
code: ErrorCode::PubkeyRequired,
detail: err.to_string(),
},
Error::PaidQuote => ErrorResponse {
code: ErrorCode::InvoiceAlreadyPaid,
detail: err.to_string(),
},
Error::DuplicatePaymentId => ErrorResponse {
code: ErrorCode::InvoiceAlreadyPaid,
detail: err.to_string(),
},
Error::Database(crate::database::Error::Duplicate) => ErrorResponse {
code: ErrorCode::InvoiceAlreadyPaid,
detail: "Invoice already paid or pending".to_string(),
},
Error::DHKE(crate::dhke::Error::TokenNotVerified) => ErrorResponse {
code: ErrorCode::TokenNotVerified,
detail: err.to_string(),
},
Error::DHKE(_) => ErrorResponse {
code: ErrorCode::Unknown(50000),
detail: err.to_string(),
},
Error::CouldNotVerifyDleq => ErrorResponse {
code: ErrorCode::TokenNotVerified,
detail: err.to_string(),
},
Error::SignatureMissingOrInvalid => ErrorResponse {
code: ErrorCode::WitnessMissingOrInvalid,
detail: err.to_string(),
},
Error::SigAllUsedInMelt => ErrorResponse {
code: ErrorCode::WitnessMissingOrInvalid,
detail: err.to_string(),
},
Error::AmountKey => ErrorResponse {
code: ErrorCode::KeysetNotFound,
detail: err.to_string(),
},
Error::KeysetUnknown(_) => ErrorResponse {
code: ErrorCode::KeysetNotFound,
detail: err.to_string(),
},
Error::NoActiveKeyset => ErrorResponse {
code: ErrorCode::KeysetInactive,
detail: err.to_string(),
},
Error::UnknownQuote => ErrorResponse {
code: ErrorCode::Unknown(50000),
detail: err.to_string(),
},
Error::MeltingDisabled => ErrorResponse {
code: ErrorCode::MintingDisabled,
detail: err.to_string(),
},
Error::PaymentPending => ErrorResponse {
code: ErrorCode::QuotePending,
detail: err.to_string(),
},
Error::UnknownPaymentState => ErrorResponse {
code: ErrorCode::Unknown(50000),
detail: err.to_string(),
},
Error::SplitValuesGreater => ErrorResponse {
code: ErrorCode::TransactionUnbalanced,
detail: err.to_string(),
},
Error::AmountOverflow => ErrorResponse {
code: ErrorCode::TransactionUnbalanced,
detail: err.to_string(),
},
Error::OverIssue => ErrorResponse {
code: ErrorCode::TransactionUnbalanced,
detail: err.to_string(),
},
Error::InvalidPaymentRequest => ErrorResponse {
code: ErrorCode::Unknown(50000),
detail: err.to_string(),
},
Error::InvoiceAmountUndefined => ErrorResponse {
code: ErrorCode::AmountlessInvoiceNotSupported,
detail: err.to_string(),
},
Error::Internal => ErrorResponse {
code: ErrorCode::Unknown(50000),
detail: err.to_string(),
},
Error::Database(_) => ErrorResponse {
code: ErrorCode::Unknown(50000),
detail: err.to_string(),
},
Error::ConcurrentUpdate => ErrorResponse {
code: ErrorCode::ConcurrentUpdate,
detail: err.to_string(),
},
Error::MaxInputsExceeded { .. } => ErrorResponse {
code: ErrorCode::MaxInputsExceeded,
detail: err.to_string()
},
Error::MaxOutputsExceeded { .. } => ErrorResponse {
code: ErrorCode::MaxOutputsExceeded,
detail: err.to_string()
},
_ => ErrorResponse {
code: ErrorCode::Unknown(50000),
detail: err.to_string(),
},
}
}
}
#[cfg(feature = "mint")]
impl From<crate::database::Error> for Error {
fn from(db_error: crate::database::Error) -> Self {
match db_error {
crate::database::Error::InvalidStateTransition(state) => match state {
crate::state::Error::Pending => Self::TokenPending,
crate::state::Error::AlreadySpent => Self::TokenAlreadySpent,
crate::state::Error::AlreadyPaid => Self::RequestAlreadyPaid,
state => Self::Database(crate::database::Error::InvalidStateTransition(state)),
},
crate::database::Error::ConcurrentUpdate => Self::ConcurrentUpdate,
db_error => Self::Database(db_error),
}
}
}
#[cfg(not(feature = "mint"))]
impl From<crate::database::Error> for Error {
fn from(db_error: crate::database::Error) -> Self {
match db_error {
crate::database::Error::ConcurrentUpdate => Self::ConcurrentUpdate,
db_error => Self::Database(db_error),
}
}
}
impl From<ErrorResponse> for Error {
fn from(err: ErrorResponse) -> Error {
match err.code {
ErrorCode::TokenNotVerified => Self::DHKE(crate::dhke::Error::TokenNotVerified),
ErrorCode::TokenAlreadySpent => Self::TokenAlreadySpent,
ErrorCode::TokenPending => Self::TokenPending,
ErrorCode::BlindedMessageAlreadySigned => Self::BlindedMessageAlreadySigned,
ErrorCode::OutputsPending => Self::TokenPending, ErrorCode::TransactionUnbalanced => Self::TransactionUnbalanced(0, 0, 0),
ErrorCode::AmountOutofLimitRange => {
Self::AmountOutofLimitRange(Amount::default(), Amount::default(), Amount::default())
}
ErrorCode::DuplicateInputs => Self::DuplicateInputs,
ErrorCode::DuplicateOutputs => Self::DuplicateOutputs,
ErrorCode::MultipleUnits => Self::MultipleUnits,
ErrorCode::UnitMismatch => Self::UnitMismatch,
ErrorCode::AmountlessInvoiceNotSupported => Self::AmountLessNotAllowed,
ErrorCode::IncorrectQuoteAmount => Self::IncorrectQuoteAmount,
ErrorCode::UnsupportedUnit => Self::UnsupportedUnit,
ErrorCode::KeysetNotFound => Self::UnknownKeySet,
ErrorCode::KeysetInactive => Self::InactiveKeyset,
ErrorCode::QuoteNotPaid => Self::UnpaidQuote,
ErrorCode::TokensAlreadyIssued => Self::IssuedQuote,
ErrorCode::MintingDisabled => Self::MintingDisabled,
ErrorCode::LightningError => Self::PaymentFailed,
ErrorCode::QuotePending => Self::PendingQuote,
ErrorCode::InvoiceAlreadyPaid => Self::RequestAlreadyPaid,
ErrorCode::QuoteExpired => Self::ExpiredQuote(0, 0),
ErrorCode::WitnessMissingOrInvalid => Self::SignatureMissingOrInvalid,
ErrorCode::PubkeyRequired => Self::PubkeyRequired,
ErrorCode::ClearAuthRequired => Self::ClearAuthRequired,
ErrorCode::ClearAuthFailed => Self::ClearAuthFailed,
ErrorCode::BlindAuthRequired => Self::BlindAuthRequired,
ErrorCode::BlindAuthFailed => Self::BlindAuthFailed,
ErrorCode::BatMintMaxExceeded => Self::InsufficientBlindAuthTokens,
ErrorCode::BatRateLimitExceeded => Self::InsufficientBlindAuthTokens,
_ => Self::UnknownErrorResponse(err.to_string()),
}
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum ErrorCode {
TokenNotVerified,
TokenAlreadySpent,
TokenPending,
BlindedMessageAlreadySigned,
OutputsPending,
TransactionUnbalanced,
AmountOutofLimitRange,
DuplicateInputs,
DuplicateOutputs,
MultipleUnits,
UnitMismatch,
AmountlessInvoiceNotSupported,
IncorrectQuoteAmount,
UnsupportedUnit,
MaxInputsExceeded,
MaxOutputsExceeded,
KeysetNotFound,
KeysetInactive,
QuoteNotPaid,
TokensAlreadyIssued,
MintingDisabled,
LightningError,
QuotePending,
InvoiceAlreadyPaid,
QuoteExpired,
WitnessMissingOrInvalid,
PubkeyRequired,
ClearAuthRequired,
ClearAuthFailed,
BlindAuthRequired,
BlindAuthFailed,
BatMintMaxExceeded,
BatRateLimitExceeded,
ConcurrentUpdate,
Unknown(u16),
}
impl ErrorCode {
pub fn from_code(code: u16) -> Self {
match code {
10001 => Self::TokenNotVerified,
11001 => Self::TokenAlreadySpent,
11002 => Self::TokenPending,
11003 => Self::BlindedMessageAlreadySigned,
11004 => Self::OutputsPending,
11005 => Self::TransactionUnbalanced,
11006 => Self::AmountOutofLimitRange,
11007 => Self::DuplicateInputs,
11008 => Self::DuplicateOutputs,
11009 => Self::MultipleUnits,
11010 => Self::UnitMismatch,
11011 => Self::AmountlessInvoiceNotSupported,
11012 => Self::IncorrectQuoteAmount,
11013 => Self::UnsupportedUnit,
11014 => Self::MaxInputsExceeded,
11015 => Self::MaxOutputsExceeded,
12001 => Self::KeysetNotFound,
12002 => Self::KeysetInactive,
20001 => Self::QuoteNotPaid,
20002 => Self::TokensAlreadyIssued,
20003 => Self::MintingDisabled,
20004 => Self::LightningError,
20005 => Self::QuotePending,
20006 => Self::InvoiceAlreadyPaid,
20007 => Self::QuoteExpired,
20008 => Self::WitnessMissingOrInvalid,
20009 => Self::PubkeyRequired,
30001 => Self::ClearAuthRequired,
30002 => Self::ClearAuthFailed,
31001 => Self::BlindAuthRequired,
31002 => Self::BlindAuthFailed,
31003 => Self::BatMintMaxExceeded,
31004 => Self::BatRateLimitExceeded,
_ => Self::Unknown(code),
}
}
pub fn to_code(&self) -> u16 {
match self {
Self::TokenNotVerified => 10001,
Self::TokenAlreadySpent => 11001,
Self::TokenPending => 11002,
Self::BlindedMessageAlreadySigned => 11003,
Self::OutputsPending => 11004,
Self::TransactionUnbalanced => 11005,
Self::AmountOutofLimitRange => 11006,
Self::DuplicateInputs => 11007,
Self::DuplicateOutputs => 11008,
Self::MultipleUnits => 11009,
Self::UnitMismatch => 11010,
Self::AmountlessInvoiceNotSupported => 11011,
Self::IncorrectQuoteAmount => 11012,
Self::UnsupportedUnit => 11013,
Self::MaxInputsExceeded => 11014,
Self::MaxOutputsExceeded => 11015,
Self::KeysetNotFound => 12001,
Self::KeysetInactive => 12002,
Self::QuoteNotPaid => 20001,
Self::TokensAlreadyIssued => 20002,
Self::MintingDisabled => 20003,
Self::LightningError => 20004,
Self::QuotePending => 20005,
Self::InvoiceAlreadyPaid => 20006,
Self::QuoteExpired => 20007,
Self::WitnessMissingOrInvalid => 20008,
Self::PubkeyRequired => 20009,
Self::ClearAuthRequired => 30001,
Self::ClearAuthFailed => 30002,
Self::BlindAuthRequired => 31001,
Self::BlindAuthFailed => 31002,
Self::BatMintMaxExceeded => 31003,
Self::BatRateLimitExceeded => 31004,
Self::ConcurrentUpdate => 50000,
Self::Unknown(code) => *code,
}
}
}
impl Serialize for ErrorCode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u16(self.to_code())
}
}
impl<'de> Deserialize<'de> for ErrorCode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let code = u16::deserialize(deserializer)?;
Ok(ErrorCode::from_code(code))
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_code())
}
}