diff --git a/crates/cdk/src/error.rs b/crates/cdk/src/error.rs index d38d8aaf..6cd1f9b6 100644 --- a/crates/cdk/src/error.rs +++ b/crates/cdk/src/error.rs @@ -3,7 +3,7 @@ use std::fmt; use std::string::FromUtf8Error; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use thiserror::Error; @@ -79,7 +79,7 @@ pub enum Error { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ErrorResponse { - pub code: u32, + pub code: ErrorCode, pub error: Option, pub detail: Option, } @@ -98,24 +98,73 @@ impl fmt::Display for ErrorResponse { impl ErrorResponse { pub fn from_json(json: &str) -> Result { - match serde_json::from_str::(json) { - Ok(res) => Ok(res), - Err(_) => Ok(Self { - code: 999, - error: Some(json.to_string()), - detail: None, - }), - } + let value: Value = serde_json::from_str(json)?; + + Self::from_value(value) } pub fn from_value(value: Value) -> Result { match serde_json::from_value::(value.clone()) { Ok(res) => Ok(res), Err(_) => Ok(Self { - code: 999, + code: ErrorCode::Unknown(999), error: Some(value.to_string()), detail: None, }), } } } + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum ErrorCode { + TokenAlreadySpent, + QuoteNotPaid, + KeysetNotFound, + Unknown(u16), +} + +impl Serialize for ErrorCode { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let code = match self { + ErrorCode::TokenAlreadySpent => 11001, + ErrorCode::QuoteNotPaid => 20001, + ErrorCode::KeysetNotFound => 12001, + ErrorCode::Unknown(code) => *code, + }; + + serializer.serialize_u16(code) + } +} + +impl<'de> Deserialize<'de> for ErrorCode { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let code = u16::deserialize(deserializer)?; + + let error_code = match code { + 11001 => ErrorCode::TokenAlreadySpent, + 20001 => ErrorCode::QuoteNotPaid, + 12001 => ErrorCode::KeysetNotFound, + c => ErrorCode::Unknown(c), + }; + + Ok(error_code) + } +} + +impl fmt::Display for ErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let code = match self { + Self::TokenAlreadySpent => 11001, + Self::QuoteNotPaid => 20001, + Self::KeysetNotFound => 12001, + Self::Unknown(code) => *code, + }; + write!(f, "{}", code) + } +} diff --git a/crates/cdk/src/lib.rs b/crates/cdk/src/lib.rs index aa62108a..1f871152 100644 --- a/crates/cdk/src/lib.rs +++ b/crates/cdk/src/lib.rs @@ -8,8 +8,6 @@ pub use lightning_invoice::{self, Bolt11Invoice}; pub mod amount; pub mod cdk_database; -#[cfg(feature = "wallet")] -pub mod client; pub mod dhke; pub mod error; #[cfg(feature = "mint")] @@ -23,8 +21,8 @@ pub mod util; pub mod wallet; pub use self::amount::Amount; -#[cfg(feature = "wallet")] -pub use self::client::HttpClient; pub use self::util::SECP256K1; +#[cfg(feature = "wallet")] +pub use self::wallet::client::HttpClient; pub type Result> = std::result::Result; diff --git a/crates/cdk/src/mint/error.rs b/crates/cdk/src/mint/error.rs new file mode 100644 index 00000000..250fff19 --- /dev/null +++ b/crates/cdk/src/mint/error.rs @@ -0,0 +1,100 @@ +use http::StatusCode; +use thiserror::Error; + +use crate::cdk_database; +use crate::error::{ErrorCode, ErrorResponse}; + +#[derive(Debug, Error)] +pub enum Error { + /// Unknown Keyset + #[error("Unknown Keyset")] + UnknownKeySet, + /// Inactive Keyset + #[error("Inactive Keyset")] + InactiveKeyset, + #[error("No key for amount")] + AmountKey, + #[error("Amount")] + Amount, + #[error("Duplicate proofs")] + DuplicateProofs, + #[error("Token Already Spent")] + TokenAlreadySpent, + #[error("Token Pending")] + TokenPending, + #[error("Quote not paid")] + UnpaidQuote, + #[error("Unknown quote")] + UnknownQuote, + #[error("Unknown secret kind")] + UnknownSecretKind, + #[error("Cannot have multiple units")] + MultipleUnits, + #[error("Blinded Message is already signed")] + BlindedMessageAlreadySigned, + #[error(transparent)] + Cashu(#[from] crate::error::Error), + #[error(transparent)] + Secret(#[from] crate::secret::Error), + #[error(transparent)] + NUT00(#[from] crate::nuts::nut00::Error), + #[error(transparent)] + NUT11(#[from] crate::nuts::nut11::Error), + #[error(transparent)] + Nut12(#[from] crate::nuts::nut12::Error), + #[error(transparent)] + Nut14(#[from] crate::nuts::nut14::Error), + /// Database Error + #[error(transparent)] + Database(#[from] cdk_database::Error), + #[error("`{0}`")] + Custom(String), +} + +impl From for cdk_database::Error { + fn from(e: Error) -> Self { + Self::Database(Box::new(e)) + } +} + +impl From for ErrorResponse { + fn from(err: Error) -> ErrorResponse { + match err { + Error::TokenAlreadySpent => ErrorResponse { + code: ErrorCode::TokenAlreadySpent, + error: Some(err.to_string()), + detail: None, + }, + _ => ErrorResponse { + code: ErrorCode::Unknown(9999), + error: Some(err.to_string()), + detail: None, + }, + } + } +} + +impl From for (StatusCode, ErrorResponse) { + fn from(err: Error) -> (StatusCode, ErrorResponse) { + (StatusCode::NOT_FOUND, err.into()) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_error_response_enum() { + let error = Error::TokenAlreadySpent; + + let response: ErrorResponse = error.into(); + + let json = serde_json::to_string(&response).unwrap(); + + let error_response: ErrorResponse = serde_json::from_str(&json).unwrap(); + + assert_eq!(response.code, error_response.code); + } +} diff --git a/crates/cdk/src/mint.rs b/crates/cdk/src/mint/mod.rs similarity index 92% rename from crates/cdk/src/mint.rs rename to crates/cdk/src/mint/mod.rs index 7ae85cd6..b294db67 100644 --- a/crates/cdk/src/mint.rs +++ b/crates/cdk/src/mint/mod.rs @@ -3,90 +3,21 @@ use std::sync::Arc; use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey}; use bitcoin::secp256k1::{self, Secp256k1}; -use http::StatusCode; +use error::Error; use serde::{Deserialize, Serialize}; -use thiserror::Error; use tokio::sync::RwLock; use tracing::{debug, error, info}; -use self::nut11::enforce_sig_flag; use crate::cdk_database::{self, MintDatabase}; use crate::dhke::{hash_to_curve, sign_message, verify_message}; -use crate::error::ErrorResponse; +use crate::nuts::nut11::enforce_sig_flag; use crate::nuts::*; use crate::types::{MeltQuote, MintQuote}; use crate::url::UncheckedUrl; use crate::util::unix_time; use crate::Amount; -#[derive(Debug, Error)] -pub enum Error { - /// Unknown Keyset - #[error("Unknown Keyset")] - UnknownKeySet, - /// Inactive Keyset - #[error("Inactive Keyset")] - InactiveKeyset, - #[error("No key for amount")] - AmountKey, - #[error("Amount")] - Amount, - #[error("Duplicate proofs")] - DuplicateProofs, - #[error("Token Spent")] - TokenSpent, - #[error("Token Pending")] - TokenPending, - #[error("Quote not paid")] - UnpaidQuote, - #[error("`{0}`")] - Custom(String), - #[error(transparent)] - Cashu(#[from] crate::error::Error), - #[error(transparent)] - Secret(#[from] crate::secret::Error), - #[error(transparent)] - NUT00(#[from] crate::nuts::nut00::Error), - #[error(transparent)] - NUT11(#[from] crate::nuts::nut11::Error), - #[error(transparent)] - Nut12(#[from] crate::nuts::nut12::Error), - #[error(transparent)] - Nut14(#[from] crate::nuts::nut14::Error), - /// Database Error - #[error(transparent)] - Database(#[from] crate::cdk_database::Error), - #[error("Unknown quote")] - UnknownQuote, - #[error("Unknown secret kind")] - UnknownSecretKind, - #[error("Cannot have multiple units")] - MultipleUnits, - #[error("Blinded Message is already signed")] - BlindedMessageAlreadySigned, -} - -impl From for cdk_database::Error { - fn from(e: Error) -> Self { - Self::Database(Box::new(e)) - } -} - -impl From for ErrorResponse { - fn from(err: Error) -> ErrorResponse { - ErrorResponse { - code: 9999, - error: Some(err.to_string()), - detail: None, - } - } -} - -impl From for (StatusCode, ErrorResponse) { - fn from(err: Error) -> (StatusCode, ErrorResponse) { - (StatusCode::NOT_FOUND, err.into()) - } -} +pub mod error; #[derive(Clone)] pub struct Mint { @@ -493,7 +424,7 @@ impl Mint { let y: PublicKey = hash_to_curve(&proof.secret.to_bytes())?; if self.localstore.get_spent_proof_by_y(&y).await?.is_some() { - return Err(Error::TokenSpent); + return Err(Error::TokenAlreadySpent); } if self.localstore.get_pending_proof_by_y(&y).await?.is_some() { diff --git a/crates/cdk/src/client.rs b/crates/cdk/src/wallet/client.rs similarity index 94% rename from crates/cdk/src/client.rs rename to crates/cdk/src/wallet/client.rs index 1d183326..562dea6c 100644 --- a/crates/cdk/src/client.rs +++ b/crates/cdk/src/wallet/client.rs @@ -2,10 +2,10 @@ use reqwest::Client; use serde_json::Value; -use thiserror::Error; use tracing::instrument; use url::Url; +use super::Error; use crate::error::ErrorResponse; use crate::nuts::{ BlindedMessage, CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysResponse, @@ -16,28 +16,6 @@ use crate::nuts::{ }; use crate::{Amount, Bolt11Invoice}; -#[derive(Debug, Error)] -pub enum Error { - /// Unknown Keyset - #[error("Url Path segments could not be joined")] - UrlPathSegments, - /// Serde Json error - #[error(transparent)] - SerdeJsonError(#[from] serde_json::Error), - /// From hex error - #[error(transparent)] - ReqwestError(#[from] reqwest::Error), - /// Unknown error response - #[error("Unknown Error response: `{0}`")] - UnknownErrorResponse(String), -} - -impl From for Error { - fn from(err: ErrorResponse) -> Error { - Self::UnknownErrorResponse(err.to_string()) - } -} - fn join_url(url: Url, paths: &[&str]) -> Result { let mut url = url; for path in paths { diff --git a/crates/cdk/src/wallet/error.rs b/crates/cdk/src/wallet/error.rs new file mode 100644 index 00000000..0715ffc5 --- /dev/null +++ b/crates/cdk/src/wallet/error.rs @@ -0,0 +1,115 @@ +use std::num::ParseIntError; + +use thiserror::Error; + +use crate::cdk_database; +use crate::error::{ErrorCode, ErrorResponse}; + +#[derive(Debug, Error)] +pub enum Error { + /// Insufficient Funds + #[error("Insufficient Funds")] + InsufficientFunds, + /// Quote Expired + #[error("Quote Expired")] + QuoteExpired, + /// Unknown Quote + #[error("Quote Unknown")] + QuoteUnknown, + /// Not active keyset + #[error("No active keyset")] + NoActiveKeyset, + /// Invalid DLEQ prood + #[error("Could not verify Dleq")] + CouldNotVerifyDleq, + /// P2PK spending conditions not met + #[error("P2PK Condition Not met `{0}`")] + P2PKConditionsNotMet(String), + /// Invalid Spending Conditions + #[error("Invalid Spending Conditions: `{0}`")] + InvalidSpendConditions(String), + /// Preimage not provided + #[error("Preimage not provided")] + PreimageNotProvided, + #[error("Unknown Key")] + UnknownKey, + /// Spending Locktime not provided + #[error("Spending condition locktime not provided")] + LocktimeNotProvided, + /// Unknown Keyset + #[error("Url Path segments could not be joined")] + UrlPathSegments, + /// Quote not paid + #[error("Quote not paid")] + QuoteNotePaid, + /// Token Already spent error + #[error("Token Already Spent Error")] + TokenAlreadySpent, + /// Keyset Not Found + #[error("Keyset Not Found")] + KeysetNotFound, + /// From hex error + #[error(transparent)] + ReqwestError(#[from] reqwest::Error), + /// Unknown error response + #[error("Unknown Error response: `{0}`")] + UnknownErrorResponse(String), + /// CDK Error + #[error(transparent)] + Cashu(#[from] crate::error::Error), + /// Cashu Url Error + #[error(transparent)] + CashuUrl(#[from] crate::url::Error), + /// Database Error + #[error(transparent)] + Database(#[from] crate::cdk_database::Error), + /// NUT00 Error + #[error(transparent)] + NUT00(#[from] crate::nuts::nut00::Error), + /// NUT01 Error + #[error(transparent)] + NUT01(#[from] crate::nuts::nut01::Error), + /// NUT11 Error + #[error(transparent)] + NUT11(#[from] crate::nuts::nut11::Error), + /// NUT12 Error + #[error(transparent)] + NUT12(#[from] crate::nuts::nut12::Error), + /// Parse int + #[error(transparent)] + ParseInt(#[from] ParseIntError), + /// Parse invoice error + #[error(transparent)] + Invoice(#[from] lightning_invoice::ParseOrSemanticError), + /// Serde Error + #[error(transparent)] + Serde(#[from] serde_json::Error), + /// Nostr Client Error + #[cfg(feature = "nostr")] + #[error(transparent)] + NostrClient(#[from] nostr_sdk::client::Error), + /// Nostr Key Error + #[cfg(feature = "nostr")] + #[error(transparent)] + NostrKey(#[from] nostr_sdk::key::Error), + /// Custom Error + #[error("`{0}`")] + Custom(String), +} + +impl From for cdk_database::Error { + fn from(e: Error) -> Self { + Self::Database(Box::new(e)) + } +} + +impl From for Error { + fn from(err: ErrorResponse) -> Error { + match err.code { + ErrorCode::QuoteNotPaid => Self::QuoteNotePaid, + ErrorCode::TokenAlreadySpent => Self::TokenAlreadySpent, + ErrorCode::KeysetNotFound => Self::KeysetNotFound, + _ => Self::UnknownErrorResponse(err.to_string()), + } + } +} diff --git a/crates/cdk/src/wallet.rs b/crates/cdk/src/wallet/mod.rs similarity index 96% rename from crates/cdk/src/wallet.rs rename to crates/cdk/src/wallet/mod.rs index 9e25e8b1..a1c348bc 100644 --- a/crates/cdk/src/wallet.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -1,7 +1,6 @@ //! Cashu Wallet use std::collections::{HashMap, HashSet}; -use std::num::ParseIntError; use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; @@ -11,17 +10,16 @@ use bitcoin::hashes::sha256::Hash as Sha256Hash; use bitcoin::hashes::Hash; use bitcoin::secp256k1::XOnlyPublicKey; use bitcoin::Network; +use error::Error; #[cfg(feature = "nostr")] use nostr_sdk::nips::nip04; #[cfg(feature = "nostr")] use nostr_sdk::{Filter, Timestamp}; -use thiserror::Error; use tokio::sync::RwLock; use tracing::instrument; use crate::amount::SplitTarget; use crate::cdk_database::{self, WalletDatabase}; -use crate::client::HttpClient; use crate::dhke::{construct_proofs, hash_to_curve}; use crate::nuts::{ nut10, nut12, Conditions, CurrencyUnit, Id, KeySet, KeySetInfo, Keys, Kind, @@ -32,78 +30,10 @@ use crate::nuts::{ use crate::types::{MeltQuote, Melted, MintQuote, ProofInfo}; use crate::url::UncheckedUrl; use crate::util::{hex, unix_time}; -use crate::{Amount, Bolt11Invoice}; +use crate::{Amount, Bolt11Invoice, HttpClient}; -#[derive(Debug, Error)] -pub enum Error { - /// Insufficient Funds - #[error("Insufficient Funds")] - InsufficientFunds, - #[error("Quote Expired")] - QuoteExpired, - #[error("Quote Unknown")] - QuoteUnknown, - #[error("No active keyset")] - NoActiveKeyset, - #[error(transparent)] - Cashu(#[from] crate::error::Error), - #[error("Could not verify Dleq")] - CouldNotVerifyDleq, - #[error("P2PK Condition Not met `{0}`")] - P2PKConditionsNotMet(String), - #[error("Invalid Spending Conditions: `{0}`")] - InvalidSpendConditions(String), - #[error("Preimage not provided")] - PreimageNotProvided, - #[error("Unknown Key")] - UnknownKey, - /// Spending Locktime not provided - #[error("Spending condition locktime not provided")] - LocktimeNotProvided, - /// Cashu Url Error - #[error(transparent)] - CashuUrl(#[from] crate::url::Error), - /// NUT11 Error - #[error(transparent)] - Client(#[from] crate::client::Error), - /// Database Error - #[error(transparent)] - Database(#[from] crate::cdk_database::Error), - /// NUT00 Error - #[error(transparent)] - NUT00(#[from] crate::nuts::nut00::Error), - /// NUT01 Error - #[error(transparent)] - NUT01(#[from] crate::nuts::nut01::Error), - /// NUT11 Error - #[error(transparent)] - NUT11(#[from] crate::nuts::nut11::Error), - /// NUT12 Error - #[error(transparent)] - NUT12(#[from] crate::nuts::nut12::Error), - /// Parse int - #[error(transparent)] - ParseInt(#[from] ParseIntError), - /// Parse invoice error - #[error(transparent)] - Invoice(#[from] lightning_invoice::ParseOrSemanticError), - #[error(transparent)] - Serde(#[from] serde_json::Error), - #[cfg(feature = "nostr")] - #[error(transparent)] - NostrClient(#[from] nostr_sdk::client::Error), - #[cfg(feature = "nostr")] - #[error(transparent)] - NostrKey(#[from] nostr_sdk::key::Error), - #[error("`{0}`")] - Custom(String), -} - -impl From for cdk_database::Error { - fn from(e: Error) -> Self { - Self::Database(Box::new(e)) - } -} +pub mod client; +pub mod error; #[derive(Clone)] pub struct Wallet {