Skip to main content

cashu/
quote_id.rs

1//! Quote ID. The specifications only define a string but CDK uses Uuid, so we use an enum to port compatibility.
2use std::fmt;
3use std::str::FromStr;
4
5use bitcoin::base64::engine::general_purpose;
6use bitcoin::base64::Engine as _;
7use serde::{de, Deserialize, Deserializer, Serialize};
8use thiserror::Error;
9use uuid::Uuid;
10
11/// Invalid UUID
12#[derive(Debug, Error)]
13pub enum QuoteIdError {
14    /// UUID Error
15    #[error("invalid UUID: {0}")]
16    Uuid(#[from] uuid::Error),
17    /// Invalid base64
18    #[error("invalid base64")]
19    Base64,
20    /// Invalid quote ID
21    #[error("neither a valid UUID nor a valid base64 string")]
22    InvalidQuoteId,
23}
24
25/// Mint Quote ID
26#[derive(Serialize, Debug, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
27#[serde(untagged)]
28pub enum QuoteId {
29    /// (Nutshell) base64 quote ID
30    BASE64(String),
31    /// UUID quote ID
32    UUID(Uuid),
33}
34
35impl QuoteId {
36    /// Create a new UUID-based MintQuoteId
37    pub fn new_uuid() -> Self {
38        Self::UUID(Uuid::new_v4())
39    }
40}
41
42impl From<Uuid> for QuoteId {
43    fn from(uuid: Uuid) -> Self {
44        Self::UUID(uuid)
45    }
46}
47
48impl fmt::Display for QuoteId {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            QuoteId::BASE64(s) => write!(f, "{s}"),
52            QuoteId::UUID(u) => write!(f, "{}", u.hyphenated()),
53        }
54    }
55}
56
57impl FromStr for QuoteId {
58    type Err = QuoteIdError;
59
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        // Try UUID first
62        if let Ok(u) = Uuid::parse_str(s) {
63            return Ok(QuoteId::UUID(u));
64        }
65
66        // Try base64: decode, then re-encode and compare to ensure canonical form
67        // Use the standard (URL/filename safe or standard) depending on your needed alphabet.
68        // Here we use standard base64.
69        match general_purpose::URL_SAFE.decode(s) {
70            Ok(_bytes) => Ok(QuoteId::BASE64(s.to_string())),
71            Err(_) => Err(QuoteIdError::InvalidQuoteId),
72        }
73    }
74}
75
76impl<'de> Deserialize<'de> for QuoteId {
77    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78    where
79        D: Deserializer<'de>,
80    {
81        // Deserialize as plain string first
82        let s = String::deserialize(deserializer)?;
83
84        // Try UUID first
85        if let Ok(u) = Uuid::parse_str(&s) {
86            return Ok(QuoteId::UUID(u));
87        }
88
89        if general_purpose::URL_SAFE.decode(&s).is_ok() {
90            return Ok(QuoteId::BASE64(s));
91        }
92
93        // Neither matched — return a helpful error
94        Err(de::Error::custom(format!(
95            "QuoteId must be either a UUID (e.g. {}) or a valid base64 string; got: {}",
96            Uuid::nil(),
97            s
98        )))
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_quote_id_display_uuid() {
108        // Test UUID display - should be hyphenated format
109        let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
110        let quote_id = QuoteId::UUID(uuid);
111        let displayed = quote_id.to_string();
112        assert_eq!(displayed, "550e8400-e29b-41d4-a716-446655440000");
113        assert!(!displayed.is_empty());
114
115        // Verify roundtrip works for UUID
116        let parsed: QuoteId = displayed.parse().unwrap();
117        assert_eq!(quote_id, parsed);
118    }
119
120    #[test]
121    fn test_quote_id_display_base64() {
122        // Test BASE64 display - should output the string as-is
123        let base64_str = "SGVsbG8gV29ybGQh"; // "Hello World!" with proper padding
124        let base64_id = QuoteId::BASE64(base64_str.to_string());
125        let displayed = base64_id.to_string();
126        assert_eq!(displayed, base64_str);
127        assert!(!displayed.is_empty());
128
129        // Verify roundtrip works for base64
130        let parsed: QuoteId = displayed.parse().unwrap();
131        assert_eq!(base64_id, parsed);
132    }
133}