diff --git a/crates/cashu/src/lib.rs b/crates/cashu/src/lib.rs index 6604775a..4b1877c7 100644 --- a/crates/cashu/src/lib.rs +++ b/crates/cashu/src/lib.rs @@ -16,6 +16,9 @@ pub use self::mint_url::MintUrl; pub use self::nuts::*; pub use self::util::SECP256K1; +#[cfg(feature = "mint")] +pub mod quote_id; + #[doc(hidden)] #[macro_export] macro_rules! ensure_cdk { @@ -25,107 +28,3 @@ macro_rules! ensure_cdk { } }; } - -#[cfg(feature = "mint")] -/// Quote ID. The specifications only define a string but CDK uses Uuid, so we use an enum to port compatibility. -pub mod quote_id { - use std::fmt; - use std::str::FromStr; - - use bitcoin::base64::engine::general_purpose; - use bitcoin::base64::Engine as _; - use serde::{de, Deserialize, Deserializer, Serialize}; - use thiserror::Error; - use uuid::Uuid; - - /// Invalid UUID - #[derive(Debug, Error)] - pub enum QuoteIdError { - /// UUID Error - #[error("invalid UUID: {0}")] - Uuid(#[from] uuid::Error), - /// Invalid base64 - #[error("invalid base64")] - Base64, - /// Invalid quote ID - #[error("neither a valid UUID nor a valid base64 string")] - InvalidQuoteId, - } - - /// Mint Quote ID - #[derive(Serialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)] - #[serde(untagged)] - pub enum QuoteId { - /// (Nutshell) base64 quote ID - BASE64(String), - /// UUID quote ID - UUID(Uuid), - } - - impl QuoteId { - /// Create a new UUID-based MintQuoteId - pub fn new_uuid() -> Self { - Self::UUID(Uuid::new_v4()) - } - } - - impl From for QuoteId { - fn from(uuid: Uuid) -> Self { - Self::UUID(uuid) - } - } - - impl fmt::Display for QuoteId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - QuoteId::BASE64(s) => write!(f, "{}", s), - QuoteId::UUID(u) => write!(f, "{}", u), - } - } - } - - impl FromStr for QuoteId { - type Err = QuoteIdError; - - fn from_str(s: &str) -> Result { - // Try UUID first - if let Ok(u) = Uuid::parse_str(s) { - return Ok(QuoteId::UUID(u)); - } - - // Try base64: decode, then re-encode and compare to ensure canonical form - // Use the standard (URL/filename safe or standard) depending on your needed alphabet. - // Here we use standard base64. - match general_purpose::URL_SAFE.decode(s) { - Ok(_bytes) => Ok(QuoteId::BASE64(s.to_string())), - Err(_) => Err(QuoteIdError::InvalidQuoteId), - } - } - } - - impl<'de> Deserialize<'de> for QuoteId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - // Deserialize as plain string first - let s = String::deserialize(deserializer)?; - - // Try UUID first - if let Ok(u) = Uuid::parse_str(&s) { - return Ok(QuoteId::UUID(u)); - } - - if general_purpose::URL_SAFE.decode(&s).is_ok() { - return Ok(QuoteId::BASE64(s)); - } - - // Neither matched — return a helpful error - Err(de::Error::custom(format!( - "QuoteId must be either a UUID (e.g. {}) or a valid base64 string; got: {}", - Uuid::nil(), - s - ))) - } - } -} diff --git a/crates/cashu/src/quote_id.rs b/crates/cashu/src/quote_id.rs new file mode 100644 index 00000000..58ccac59 --- /dev/null +++ b/crates/cashu/src/quote_id.rs @@ -0,0 +1,100 @@ +//! Quote ID. The specifications only define a string but CDK uses Uuid, so we use an enum to port compatibility. +use std::fmt; +use std::str::FromStr; + +use bitcoin::base64::engine::general_purpose; +use bitcoin::base64::Engine as _; +use serde::{de, Deserialize, Deserializer, Serialize}; +use thiserror::Error; +use uuid::Uuid; + +/// Invalid UUID +#[derive(Debug, Error)] +pub enum QuoteIdError { + /// UUID Error + #[error("invalid UUID: {0}")] + Uuid(#[from] uuid::Error), + /// Invalid base64 + #[error("invalid base64")] + Base64, + /// Invalid quote ID + #[error("neither a valid UUID nor a valid base64 string")] + InvalidQuoteId, +} + +/// Mint Quote ID +#[derive(Serialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)] +#[serde(untagged)] +pub enum QuoteId { + /// (Nutshell) base64 quote ID + BASE64(String), + /// UUID quote ID + UUID(Uuid), +} + +impl QuoteId { + /// Create a new UUID-based MintQuoteId + pub fn new_uuid() -> Self { + Self::UUID(Uuid::new_v4()) + } +} + +impl From for QuoteId { + fn from(uuid: Uuid) -> Self { + Self::UUID(uuid) + } +} + +impl fmt::Display for QuoteId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + QuoteId::BASE64(s) => write!(f, "{}", s), + QuoteId::UUID(u) => write!(f, "{}", u.hyphenated()), + } + } +} + +impl FromStr for QuoteId { + type Err = QuoteIdError; + + fn from_str(s: &str) -> Result { + // Try UUID first + if let Ok(u) = Uuid::parse_str(s) { + return Ok(QuoteId::UUID(u)); + } + + // Try base64: decode, then re-encode and compare to ensure canonical form + // Use the standard (URL/filename safe or standard) depending on your needed alphabet. + // Here we use standard base64. + match general_purpose::URL_SAFE.decode(s) { + Ok(_bytes) => Ok(QuoteId::BASE64(s.to_string())), + Err(_) => Err(QuoteIdError::InvalidQuoteId), + } + } +} + +impl<'de> Deserialize<'de> for QuoteId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Deserialize as plain string first + let s = String::deserialize(deserializer)?; + + // Try UUID first + if let Ok(u) = Uuid::parse_str(&s) { + return Ok(QuoteId::UUID(u)); + } + + if general_purpose::URL_SAFE.decode(&s).is_ok() { + return Ok(QuoteId::BASE64(s)); + } + + // Neither matched — return a helpful error + Err(de::Error::custom(format!( + "QuoteId must be either a UUID (e.g. {}) or a valid base64 string; got: {}", + Uuid::nil(), + s + ))) + } +} diff --git a/crates/cdk-common/src/database/mint/mod.rs b/crates/cdk-common/src/database/mint/mod.rs index 3f3c1d3a..a48d29c3 100644 --- a/crates/cdk-common/src/database/mint/mod.rs +++ b/crates/cdk-common/src/database/mint/mod.rs @@ -5,7 +5,6 @@ use std::collections::HashMap; use async_trait::async_trait; use cashu::quote_id::QuoteId; use cashu::{Amount, MintInfo}; -use uuid::Uuid; use super::Error; use crate::common::QuoteTTL; @@ -89,7 +88,7 @@ pub trait QuotesTransaction<'a> { /// Get [`mint::MeltQuote`] and lock it for update in this transaction async fn get_melt_quote( &mut self, - quote_id: &Uuid, + quote_id: &QuoteId, ) -> Result, Self::Err>; /// Add [`mint::MeltQuote`] async fn add_melt_quote(&mut self, quote: mint::MeltQuote) -> Result<(), Self::Err>; @@ -111,7 +110,7 @@ pub trait QuotesTransaction<'a> { payment_proof: Option, ) -> Result<(MeltQuoteState, mint::MeltQuote), Self::Err>; /// Remove [`mint::MeltQuote`] - async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err>; + async fn remove_melt_quote(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err>; /// Get all [`MintMintQuote`]s and lock it for update in this transaction async fn get_mint_quote_by_request( &mut self, @@ -165,7 +164,11 @@ pub trait ProofsTransaction<'a> { /// /// Adds proofs to the database. The database should error if the proof already exits, with a /// `AttemptUpdateSpentProof` if the proof is already spent or a `Duplicate` error otherwise. - async fn add_proofs(&mut self, proof: Proofs, quote_id: Option) -> Result<(), Self::Err>; + async fn add_proofs( + &mut self, + proof: Proofs, + quote_id: Option, + ) -> Result<(), Self::Err>; /// Updates the proofs to a given states and return the previous states async fn update_proofs_states( &mut self, @@ -177,7 +180,7 @@ pub trait ProofsTransaction<'a> { async fn remove_proofs( &mut self, ys: &[PublicKey], - quote_id: Option, + quote_id: Option, ) -> Result<(), Self::Err>; } @@ -190,7 +193,10 @@ pub trait ProofsDatabase { /// Get [`Proofs`] by ys async fn get_proofs_by_ys(&self, ys: &[PublicKey]) -> Result>, Self::Err>; /// Get ys by quote id - async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result, Self::Err>; + async fn get_proof_ys_by_quote_id( + &self, + quote_id: &QuoteId, + ) -> Result, Self::Err>; /// Get [`Proofs`] state async fn get_proofs_states(&self, ys: &[PublicKey]) -> Result>, Self::Err>; /// Get [`Proofs`] by state diff --git a/crates/cdk-common/src/database/mint/test.rs b/crates/cdk-common/src/database/mint/test.rs index d8b69d08..e7455fd2 100644 --- a/crates/cdk-common/src/database/mint/test.rs +++ b/crates/cdk-common/src/database/mint/test.rs @@ -87,7 +87,7 @@ where { let keyset_id = setup_keyset(&db).await; - let quote_id = Uuid::max(); + let quote_id = QuoteId::new_uuid(); let proofs = vec![ Proof { @@ -110,7 +110,9 @@ where // Add proofs to database let mut tx = Database::begin_transaction(&db).await.unwrap(); - tx.add_proofs(proofs.clone(), Some(quote_id)).await.unwrap(); + tx.add_proofs(proofs.clone(), Some(quote_id.clone())) + .await + .unwrap(); assert!(tx.commit().await.is_ok()); let proofs_from_db = db.get_proofs_by_ys(&[proofs[0].c, proofs[1].c]).await; diff --git a/crates/cdk-sql-common/Cargo.toml b/crates/cdk-sql-common/Cargo.toml index 2c54c957..7785ff4d 100644 --- a/crates/cdk-sql-common/Cargo.toml +++ b/crates/cdk-sql-common/Cargo.toml @@ -27,5 +27,4 @@ tokio.workspace = true serde.workspace = true serde_json.workspace = true lightning-invoice.workspace = true -uuid.workspace = true once_cell.workspace = true diff --git a/crates/cdk-sql-common/src/mint/mod.rs b/crates/cdk-sql-common/src/mint/mod.rs index 02f28150..ec7ae447 100644 --- a/crates/cdk-sql-common/src/mint/mod.rs +++ b/crates/cdk-sql-common/src/mint/mod.rs @@ -37,7 +37,6 @@ use cdk_common::{ use lightning_invoice::Bolt11Invoice; use migrations::MIGRATIONS; use tracing::instrument; -use uuid::Uuid; use crate::common::migrate; use crate::database::{ConnectionWithTransaction, DatabaseExecutor}; @@ -170,7 +169,7 @@ where async fn add_proofs( &mut self, proofs: Proofs, - quote_id: Option, + quote_id: Option, ) -> Result<(), Self::Err> { let current_time = unix_time(); @@ -213,7 +212,7 @@ where proof.witness.map(|w| serde_json::to_string(&w).unwrap()), ) .bind("state", "UNSPENT".to_string()) - .bind("quote_id", quote_id.map(|q| q.hyphenated().to_string())) + .bind("quote_id", quote_id.clone().map(|q| q.to_string())) .bind("created_time", current_time as i64) .execute(&self.inner) .await?; @@ -254,7 +253,7 @@ where async fn remove_proofs( &mut self, ys: &[PublicKey], - _quote_id: Option, + _quote_id: Option, ) -> Result<(), Self::Err> { if ys.is_empty() { return Ok(()); @@ -328,13 +327,7 @@ where quote_id=:quote_id "#, )? - .bind( - "quote_id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("quote_id", quote_id.to_string()) .fetch_all(conn) .await? .into_iter() @@ -363,13 +356,7 @@ FROM mint_quote_issued WHERE quote_id=:quote_id "#, )? - .bind( - "quote_id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("quote_id", quote_id.to_string()) .fetch_all(conn) .await? .into_iter() @@ -591,13 +578,7 @@ where FOR UPDATE "#, )? - .bind( - "quote_id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("quote_id", quote_id.to_string()) .fetch_one(&self.inner) .await .inspect_err(|err| { @@ -632,13 +613,7 @@ where "#, )? .bind("amount_paid", new_amount_paid.to_i64()) - .bind( - "quote_id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("quote_id", quote_id.to_string()) .execute(&self.inner) .await .inspect_err(|err| { @@ -653,13 +628,7 @@ where VALUES (:quote_id, :payment_id, :amount, :timestamp) "#, )? - .bind( - "quote_id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("quote_id", quote_id.to_string()) .bind("payment_id", payment_id) .bind("amount", amount_paid.to_i64()) .bind("timestamp", unix_time() as i64) @@ -688,13 +657,7 @@ where FOR UPDATE "#, )? - .bind( - "quote_id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("quote_id", quote_id.to_string()) .fetch_one(&self.inner) .await .inspect_err(|err| { @@ -722,13 +685,7 @@ where "#, )? .bind("amount_issued", new_amount_issued.to_i64()) - .bind( - "quote_id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("quote_id", quote_id.to_string()) .execute(&self.inner) .await .inspect_err(|err| { @@ -744,13 +701,7 @@ INSERT INTO mint_quote_issued VALUES (:quote_id, :amount, :timestamp); "#, )? - .bind( - "quote_id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("quote_id", quote_id.to_string()) .bind("amount", amount_issued.to_i64()) .bind("timestamp", current_time as i64) .execute(&self.inner) @@ -792,13 +743,7 @@ VALUES (:quote_id, :amount, :timestamp); async fn remove_mint_quote(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> { query(r#"DELETE FROM mint_quote WHERE id=:id"#)? - .bind( - "id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("id", quote_id.to_string()) .execute(&self.inner) .await?; Ok(()) @@ -861,10 +806,7 @@ VALUES (:quote_id, :amount, :timestamp); query(r#"UPDATE melt_quote SET request_lookup_id = :new_req_id, request_lookup_id_kind = :new_kind WHERE id = :id"#)? .bind("new_req_id", new_request_lookup_id.to_string()) .bind("new_kind",new_request_lookup_id.kind() ) - .bind("id", match quote_id { - QuoteId::BASE64(s) => s.to_string(), - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - }) + .bind("id", quote_id.to_string()) .execute(&self.inner) .await?; Ok(()) @@ -900,13 +842,7 @@ VALUES (:quote_id, :amount, :timestamp); AND state != :state "#, )? - .bind( - "id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("id", quote_id.to_string()) .bind("state", state.to_string()) .fetch_one(&self.inner) .await? @@ -920,22 +856,13 @@ VALUES (:quote_id, :amount, :timestamp); .bind("state", state.to_string()) .bind("paid_time", current_time as i64) .bind("payment_preimage", payment_proof) - .bind("id", match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }) + .bind("id", quote_id.to_string()) .execute(&self.inner) .await } else { query(r#"UPDATE melt_quote SET state = :state WHERE id = :id"#)? .bind("state", state.to_string()) - .bind( - "id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("id", quote_id.to_string()) .execute(&self.inner) .await }; @@ -954,14 +881,14 @@ VALUES (:quote_id, :amount, :timestamp); Ok((old_state, quote)) } - async fn remove_melt_quote(&mut self, quote_id: &Uuid) -> Result<(), Self::Err> { + async fn remove_melt_quote(&mut self, quote_id: &QuoteId) -> Result<(), Self::Err> { query( r#" DELETE FROM melt_quote WHERE id=? "#, )? - .bind("id", quote_id.as_hyphenated().to_string()) + .bind("id", quote_id.to_string()) .execute(&self.inner) .await?; @@ -993,13 +920,7 @@ VALUES (:quote_id, :amount, :timestamp); FOR UPDATE "#, )? - .bind( - "id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("id", quote_id.to_string()) .fetch_one(&self.inner) .await? .map(|row| sql_row_to_mint_quote(row, payments, issuance)) @@ -1008,7 +929,7 @@ VALUES (:quote_id, :amount, :timestamp); async fn get_melt_quote( &mut self, - quote_id: &Uuid, + quote_id: &QuoteId, ) -> Result, Self::Err> { Ok(query( r#" @@ -1033,7 +954,7 @@ VALUES (:quote_id, :amount, :timestamp); id=:id "#, )? - .bind("id", quote_id.as_hyphenated().to_string()) + .bind("id", quote_id.to_string()) .fetch_one(&self.inner) .await? .map(sql_row_to_melt_quote) @@ -1157,13 +1078,7 @@ where mint_quote WHERE id = :id"#, )? - .bind( - "id", - match quote_id { - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - QuoteId::BASE64(s) => s.to_string(), - }, - ) + .bind("id", quote_id.to_string()) .fetch_one(&*conn) .await? .map(|row| sql_row_to_mint_quote(row, payments, issuance)) @@ -1319,13 +1234,7 @@ where id=:id "#, )? - .bind( - "id", - match quote_id { - QuoteId::BASE64(s) => s.to_string(), - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - }, - ) + .bind("id", quote_id.to_string()) .fetch_one(&*conn) .await? .map(sql_row_to_melt_quote) @@ -1406,7 +1315,10 @@ where Ok(ys.iter().map(|y| proofs.remove(y)).collect()) } - async fn get_proof_ys_by_quote_id(&self, quote_id: &Uuid) -> Result, Self::Err> { + async fn get_proof_ys_by_quote_id( + &self, + quote_id: &QuoteId, + ) -> Result, Self::Err> { let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?; Ok(query( r#" @@ -1422,7 +1334,7 @@ where quote_id = :quote_id "#, )? - .bind("quote_id", quote_id.as_hyphenated().to_string()) + .bind("quote_id", quote_id.to_string()) .fetch_all(&*conn) .await? .into_iter() @@ -1661,13 +1573,7 @@ where quote_id=:quote_id "#, )? - .bind( - "quote_id", - match quote_id { - QuoteId::BASE64(s) => s.to_string(), - QuoteId::UUID(u) => u.as_hyphenated().to_string(), - }, - ) + .bind("quote_id", quote_id.to_string()) .fetch_all(&*conn) .await? .into_iter()