Browse Source

refactor: nut04 and nut05 (#749)

thesimplekid 1 month ago
parent
commit
b63dc1045d
40 changed files with 1133 additions and 603 deletions
  1. 7 6
      crates/cashu/src/nuts/mod.rs
  2. 238 126
      crates/cashu/src/nuts/nut04.rs
  3. 241 292
      crates/cashu/src/nuts/nut05.rs
  4. 17 2
      crates/cashu/src/nuts/nut06.rs
  5. 3 2
      crates/cashu/src/nuts/nut08.rs
  6. 8 8
      crates/cashu/src/nuts/nut20.rs
  7. 411 0
      crates/cashu/src/nuts/nut23.rs
  8. 3 3
      crates/cdk-axum/src/auth.rs
  9. 9 10
      crates/cdk-axum/src/lib.rs
  10. 11 15
      crates/cdk-axum/src/router_handlers.rs
  11. 4 4
      crates/cdk-common/src/database/mint/mod.rs
  12. 5 2
      crates/cdk-common/src/error.rs
  13. 3 0
      crates/cdk-common/src/payment.rs
  14. 5 8
      crates/cdk-integration-tests/src/init_pure_tests.rs
  15. 5 5
      crates/cdk-integration-tests/tests/fake_auth.rs
  16. 11 11
      crates/cdk-integration-tests/tests/fake_wallet.rs
  17. 2 2
      crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs
  18. 3 3
      crates/cdk-integration-tests/tests/integration_tests_pure.rs
  19. 2 2
      crates/cdk-integration-tests/tests/regtest.rs
  20. 9 4
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04.rs
  21. 12 3
      crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut05.rs
  22. 16 5
      crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto
  23. 28 13
      crates/cdk-mint-rpc/src/proto/server.rs
  24. 7 7
      crates/cdk-payment-processor/src/proto/mod.rs
  25. 4 4
      crates/cdk-redb/src/mint/mod.rs
  26. 3 0
      crates/cdk-sqlite/src/mint/error.rs
  27. 2 2
      crates/cdk-sqlite/src/mint/memory.rs
  28. 6 7
      crates/cdk-sqlite/src/mint/mod.rs
  29. 3 0
      crates/cdk-sqlite/src/wallet/error.rs
  30. 8 2
      crates/cdk/src/mint/builder.rs
  31. 3 3
      crates/cdk/src/mint/issue/auth.rs
  32. 5 5
      crates/cdk/src/mint/issue/issue_nut04.rs
  33. 12 11
      crates/cdk/src/mint/melt.rs
  34. 2 2
      crates/cdk/src/mint/mod.rs
  35. 2 5
      crates/cdk/src/wallet/auth/auth_connector.rs
  36. 2 2
      crates/cdk/src/wallet/melt.rs
  37. 8 5
      crates/cdk/src/wallet/mint.rs
  38. 6 12
      crates/cdk/src/wallet/mint_connector/http_client.rs
  39. 5 8
      crates/cdk/src/wallet/mint_connector/mod.rs
  40. 2 2
      crates/cdk/src/wallet/subscription/http.rs

+ 7 - 6
crates/cashu/src/nuts/mod.rs

@@ -23,6 +23,7 @@ pub mod nut17;
 pub mod nut18;
 pub mod nut19;
 pub mod nut20;
+pub mod nut23;
 
 #[cfg(feature = "auth")]
 mod auth;
@@ -45,13 +46,9 @@ pub use nut02::{Id, KeySet, KeySetInfo, KeysetResponse};
 #[cfg(feature = "wallet")]
 pub use nut03::PreSwap;
 pub use nut03::{SwapRequest, SwapResponse};
-pub use nut04::{
-    MintBolt11Request, MintBolt11Response, MintMethodSettings, MintQuoteBolt11Request,
-    MintQuoteBolt11Response, QuoteState as MintQuoteState, Settings as NUT04Settings,
-};
+pub use nut04::{MintMethodSettings, MintRequest, MintResponse, Settings as NUT04Settings};
 pub use nut05::{
-    MeltBolt11Request, MeltMethodSettings, MeltOptions, MeltQuoteBolt11Request,
-    MeltQuoteBolt11Response, QuoteState as MeltQuoteState, Settings as NUT05Settings,
+    MeltMethodSettings, MeltRequest, QuoteState as MeltQuoteState, Settings as NUT05Settings,
 };
 pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts};
 pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};
@@ -66,3 +63,7 @@ pub use nut18::{
     PaymentRequest, PaymentRequestBuilder, PaymentRequestPayload, Transport, TransportBuilder,
     TransportType,
 };
+pub use nut23::{
+    MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintQuoteBolt11Request,
+    MintQuoteBolt11Response, QuoteState as MintQuoteState,
+};

+ 238 - 126
crates/cashu/src/nuts/nut04.rs

@@ -3,16 +3,17 @@
 //! <https://github.com/cashubtc/nuts/blob/main/04.md>
 
 use std::fmt;
+#[cfg(feature = "mint")]
 use std::str::FromStr;
 
-use serde::de::DeserializeOwned;
+use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
+use serde::ser::{SerializeStruct, Serializer};
 use serde::{Deserialize, Serialize};
 use thiserror::Error;
 #[cfg(feature = "mint")]
 use uuid::Uuid;
 
 use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod};
-use super::{MintQuoteState, PublicKey};
 use crate::Amount;
 
 /// NUT04 Error
@@ -26,124 +27,11 @@ pub enum Error {
     AmountOverflow,
 }
 
-/// Mint quote request [NUT-04]
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-pub struct MintQuoteBolt11Request {
-    /// Amount
-    pub amount: Amount,
-    /// Unit wallet would like to pay with
-    pub unit: CurrencyUnit,
-    /// Memo to create the invoice with
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub description: Option<String>,
-    /// NUT-19 Pubkey
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub pubkey: Option<PublicKey>,
-}
-
-/// Possible states of a quote
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
-#[serde(rename_all = "UPPERCASE")]
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MintQuoteState))]
-pub enum QuoteState {
-    /// Quote has not been paid
-    #[default]
-    Unpaid,
-    /// Quote has been paid and wallet can mint
-    Paid,
-    /// Minting is in progress
-    /// **Note:** This state is to be used internally but is not part of the
-    /// nut.
-    Pending,
-    /// ecash issued for quote
-    Issued,
-}
-
-impl fmt::Display for QuoteState {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        match self {
-            Self::Unpaid => write!(f, "UNPAID"),
-            Self::Paid => write!(f, "PAID"),
-            Self::Pending => write!(f, "PENDING"),
-            Self::Issued => write!(f, "ISSUED"),
-        }
-    }
-}
-
-impl FromStr for QuoteState {
-    type Err = Error;
-
-    fn from_str(state: &str) -> Result<Self, Self::Err> {
-        match state {
-            "PENDING" => Ok(Self::Pending),
-            "PAID" => Ok(Self::Paid),
-            "UNPAID" => Ok(Self::Unpaid),
-            "ISSUED" => Ok(Self::Issued),
-            _ => Err(Error::UnknownState),
-        }
-    }
-}
-
-/// Mint quote response [NUT-04]
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-#[serde(bound = "Q: Serialize + DeserializeOwned")]
-pub struct MintQuoteBolt11Response<Q> {
-    /// Quote Id
-    pub quote: Q,
-    /// Payment request to fulfil
-    pub request: String,
-    /// Amount
-    // REVIEW: This is now required in the spec, we should remove the option once all mints update
-    pub amount: Option<Amount>,
-    /// Unit
-    // REVIEW: This is now required in the spec, we should remove the option once all mints update
-    pub unit: Option<CurrencyUnit>,
-    /// Quote State
-    pub state: MintQuoteState,
-    /// Unix timestamp until the quote is valid
-    pub expiry: Option<u64>,
-    /// NUT-19 Pubkey
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub pubkey: Option<PublicKey>,
-}
-
-impl<Q: ToString> MintQuoteBolt11Response<Q> {
-    /// Convert the MintQuote with a quote type Q to a String
-    pub fn to_string_id(&self) -> MintQuoteBolt11Response<String> {
-        MintQuoteBolt11Response {
-            quote: self.quote.to_string(),
-            request: self.request.clone(),
-            state: self.state,
-            expiry: self.expiry,
-            pubkey: self.pubkey,
-            amount: self.amount,
-            unit: self.unit.clone(),
-        }
-    }
-}
-
-#[cfg(feature = "mint")]
-impl From<MintQuoteBolt11Response<Uuid>> for MintQuoteBolt11Response<String> {
-    fn from(value: MintQuoteBolt11Response<Uuid>) -> Self {
-        Self {
-            quote: value.quote.to_string(),
-            request: value.request,
-            state: value.state,
-            expiry: value.expiry,
-            pubkey: value.pubkey,
-            amount: value.amount,
-            unit: value.unit.clone(),
-        }
-    }
-}
-
 /// Mint request [NUT-04]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 #[serde(bound = "Q: Serialize + DeserializeOwned")]
-pub struct MintBolt11Request<Q> {
+pub struct MintRequest<Q> {
     /// Quote id
     #[cfg_attr(feature = "swagger", schema(max_length = 1_000))]
     pub quote: Q,
@@ -156,10 +44,10 @@ pub struct MintBolt11Request<Q> {
 }
 
 #[cfg(feature = "mint")]
-impl TryFrom<MintBolt11Request<String>> for MintBolt11Request<Uuid> {
+impl TryFrom<MintRequest<String>> for MintRequest<Uuid> {
     type Error = uuid::Error;
 
-    fn try_from(value: MintBolt11Request<String>) -> Result<Self, Self::Error> {
+    fn try_from(value: MintRequest<String>) -> Result<Self, Self::Error> {
         Ok(Self {
             quote: Uuid::from_str(&value.quote)?,
             outputs: value.outputs,
@@ -168,7 +56,7 @@ impl TryFrom<MintBolt11Request<String>> for MintBolt11Request<Uuid> {
     }
 }
 
-impl<Q> MintBolt11Request<Q> {
+impl<Q> MintRequest<Q> {
     /// Total [`Amount`] of outputs
     pub fn total_amount(&self) -> Result<Amount, Error> {
         Amount::try_sum(
@@ -183,13 +71,13 @@ impl<Q> MintBolt11Request<Q> {
 /// Mint response [NUT-04]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-pub struct MintBolt11Response {
+pub struct MintResponse {
     /// Blinded Signatures
     pub signatures: Vec<BlindSignature>,
 }
 
 /// Mint Method Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct MintMethodSettings {
     /// Payment Method e.g. bolt11
@@ -197,14 +85,168 @@ pub struct MintMethodSettings {
     /// Currency Unit e.g. sat
     pub unit: CurrencyUnit,
     /// Min Amount
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub min_amount: Option<Amount>,
     /// Max Amount
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub max_amount: Option<Amount>,
-    /// Quote Description
-    #[serde(default)]
-    pub description: bool,
+    /// Options
+    pub options: Option<MintMethodOptions>,
+}
+
+impl Serialize for MintMethodSettings {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut num_fields = 3; // method and unit are always present
+        if self.min_amount.is_some() {
+            num_fields += 1;
+        }
+        if self.max_amount.is_some() {
+            num_fields += 1;
+        }
+
+        let mut description_in_top_level = false;
+        if let Some(MintMethodOptions::Bolt11 { description }) = &self.options {
+            if *description {
+                num_fields += 1;
+                description_in_top_level = true;
+            }
+        }
+
+        let mut state = serializer.serialize_struct("MintMethodSettings", num_fields)?;
+
+        state.serialize_field("method", &self.method)?;
+        state.serialize_field("unit", &self.unit)?;
+
+        if let Some(min_amount) = &self.min_amount {
+            state.serialize_field("min_amount", min_amount)?;
+        }
+
+        if let Some(max_amount) = &self.max_amount {
+            state.serialize_field("max_amount", max_amount)?;
+        }
+
+        // If there's a description flag in Bolt11 options, add it at the top level
+        if description_in_top_level {
+            state.serialize_field("description", &true)?;
+        }
+
+        state.end()
+    }
+}
+
+struct MintMethodSettingsVisitor;
+
+impl<'de> Visitor<'de> for MintMethodSettingsVisitor {
+    type Value = MintMethodSettings;
+
+    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        formatter.write_str("a MintMethodSettings structure")
+    }
+
+    fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+    where
+        M: MapAccess<'de>,
+    {
+        let mut method: Option<PaymentMethod> = None;
+        let mut unit: Option<CurrencyUnit> = None;
+        let mut min_amount: Option<Amount> = None;
+        let mut max_amount: Option<Amount> = None;
+        let mut description: Option<bool> = None;
+
+        while let Some(key) = map.next_key::<String>()? {
+            match key.as_str() {
+                "method" => {
+                    if method.is_some() {
+                        return Err(de::Error::duplicate_field("method"));
+                    }
+                    method = Some(map.next_value()?);
+                }
+                "unit" => {
+                    if unit.is_some() {
+                        return Err(de::Error::duplicate_field("unit"));
+                    }
+                    unit = Some(map.next_value()?);
+                }
+                "min_amount" => {
+                    if min_amount.is_some() {
+                        return Err(de::Error::duplicate_field("min_amount"));
+                    }
+                    min_amount = Some(map.next_value()?);
+                }
+                "max_amount" => {
+                    if max_amount.is_some() {
+                        return Err(de::Error::duplicate_field("max_amount"));
+                    }
+                    max_amount = Some(map.next_value()?);
+                }
+                "description" => {
+                    if description.is_some() {
+                        return Err(de::Error::duplicate_field("description"));
+                    }
+                    description = Some(map.next_value()?);
+                }
+                "options" => {
+                    // If there are explicit options, they take precedence, except the description
+                    // field which we will handle specially
+                    let options: Option<MintMethodOptions> = map.next_value()?;
+
+                    if let Some(MintMethodOptions::Bolt11 {
+                        description: desc_from_options,
+                    }) = options
+                    {
+                        // If we already found a top-level description, use that instead
+                        if description.is_none() {
+                            description = Some(desc_from_options);
+                        }
+                    }
+                }
+                _ => {
+                    // Skip unknown fields
+                    let _: serde::de::IgnoredAny = map.next_value()?;
+                }
+            }
+        }
+
+        let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
+        let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
+
+        // Create options based on the method and the description flag
+        let options = if method == PaymentMethod::Bolt11 {
+            description.map(|description| MintMethodOptions::Bolt11 { description })
+        } else {
+            None
+        };
+
+        Ok(MintMethodSettings {
+            method,
+            unit,
+            min_amount,
+            max_amount,
+            options,
+        })
+    }
+}
+
+impl<'de> Deserialize<'de> for MintMethodSettings {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        deserializer.deserialize_map(MintMethodSettingsVisitor)
+    }
+}
+
+/// Mint Method settings options
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(untagged)]
+pub enum MintMethodOptions {
+    /// Bolt11 Options
+    Bolt11 {
+        /// Mint supports setting bolt11 description
+        description: bool,
+    },
 }
 
 /// Mint Settings
@@ -250,3 +292,73 @@ impl Settings {
             .map(|index| self.methods.remove(index))
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use serde_json::{from_str, json, to_string};
+
+    use super::*;
+
+    #[test]
+    fn test_mint_method_settings_top_level_description() {
+        // Create JSON with top-level description
+        let json_str = r#"{
+            "method": "bolt11",
+            "unit": "sat",
+            "min_amount": 0,
+            "max_amount": 10000,
+            "description": true
+        }"#;
+
+        // Deserialize it
+        let settings: MintMethodSettings = from_str(json_str).unwrap();
+
+        // Check that description was correctly moved to options
+        assert_eq!(settings.method, PaymentMethod::Bolt11);
+        assert_eq!(settings.unit, CurrencyUnit::Sat);
+        assert_eq!(settings.min_amount, Some(Amount::from(0)));
+        assert_eq!(settings.max_amount, Some(Amount::from(10000)));
+
+        match settings.options {
+            Some(MintMethodOptions::Bolt11 { description }) => {
+                assert_eq!(description, true);
+            }
+            _ => panic!("Expected Bolt11 options with description = true"),
+        }
+
+        // Serialize it back
+        let serialized = to_string(&settings).unwrap();
+        let parsed: serde_json::Value = from_str(&serialized).unwrap();
+
+        // Verify the description is at the top level
+        assert_eq!(parsed["description"], json!(true));
+    }
+
+    #[test]
+    fn test_both_description_locations() {
+        // Create JSON with description in both places (top level and in options)
+        let json_str = r#"{
+            "method": "bolt11",
+            "unit": "sat",
+            "min_amount": 0,
+            "max_amount": 10000,
+            "description": true,
+            "options": {
+                "description": false
+            }
+        }"#;
+
+        // Deserialize it - top level should take precedence
+        let settings: MintMethodSettings = from_str(json_str).unwrap();
+
+        match settings.options {
+            Some(MintMethodOptions::Bolt11 { description }) => {
+                assert_eq!(
+                    description, true,
+                    "Top-level description should take precedence"
+                );
+            }
+            _ => panic!("Expected Bolt11 options with description = true"),
+        }
+    }
+}

+ 241 - 292
crates/cashu/src/nuts/nut05.rs

@@ -5,18 +5,16 @@
 use std::fmt;
 use std::str::FromStr;
 
-use serde::de::DeserializeOwned;
-use serde::{Deserialize, Deserializer, Serialize};
-use serde_json::Value;
+use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
+use serde::ser::{SerializeStruct, Serializer};
+use serde::{Deserialize, Serialize};
 use thiserror::Error;
 #[cfg(feature = "mint")]
 use uuid::Uuid;
 
-use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
-use super::nut15::Mpp;
+use super::nut00::{BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
 use super::ProofsMethods;
-use crate::nuts::MeltQuoteState;
-use crate::{Amount, Bolt11Invoice};
+use crate::Amount;
 
 /// NUT05 Error
 #[derive(Debug, Error)]
@@ -27,118 +25,11 @@ pub enum Error {
     /// Amount overflow
     #[error("Amount Overflow")]
     AmountOverflow,
-    /// Invalid Amount
-    #[error("Invalid Request")]
-    InvalidAmountRequest,
     /// Unsupported unit
     #[error("Unsupported unit")]
     UnsupportedUnit,
 }
 
-/// Melt quote request [NUT-05]
-#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-pub struct MeltQuoteBolt11Request {
-    /// Bolt11 invoice to be paid
-    #[cfg_attr(feature = "swagger", schema(value_type = String))]
-    pub request: Bolt11Invoice,
-    /// Unit wallet would like to pay with
-    pub unit: CurrencyUnit,
-    /// Payment Options
-    pub options: Option<MeltOptions>,
-}
-
-/// Melt Options
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(untagged)]
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-pub enum MeltOptions {
-    /// Mpp Options
-    Mpp {
-        /// MPP
-        mpp: Mpp,
-    },
-    /// Amountless options
-    Amountless {
-        /// Amountless
-        amountless: Amountless,
-    },
-}
-
-impl MeltOptions {
-    /// Create new [`MeltOptions::Mpp`]
-    pub fn new_mpp<A>(amount: A) -> Self
-    where
-        A: Into<Amount>,
-    {
-        Self::Mpp {
-            mpp: Mpp {
-                amount: amount.into(),
-            },
-        }
-    }
-
-    /// Create new [`MeltOptions::Amountless`]
-    pub fn new_amountless<A>(amount_msat: A) -> Self
-    where
-        A: Into<Amount>,
-    {
-        Self::Amountless {
-            amountless: Amountless {
-                amount_msat: amount_msat.into(),
-            },
-        }
-    }
-
-    /// Payment amount
-    pub fn amount_msat(&self) -> Amount {
-        match self {
-            Self::Mpp { mpp } => mpp.amount,
-            Self::Amountless { amountless } => amountless.amount_msat,
-        }
-    }
-}
-
-/// Amountless payment
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-pub struct Amountless {
-    /// Amount to pay in msat
-    pub amount_msat: Amount,
-}
-
-impl MeltQuoteBolt11Request {
-    /// Amount from [`MeltQuoteBolt11Request`]
-    ///
-    /// Amount can either be defined in the bolt11 invoice,
-    /// in the request for an amountless bolt11 or in MPP option.
-    pub fn amount_msat(&self) -> Result<Amount, Error> {
-        let MeltQuoteBolt11Request {
-            request,
-            unit: _,
-            options,
-            ..
-        } = self;
-
-        match options {
-            None => Ok(request
-                .amount_milli_satoshis()
-                .ok_or(Error::InvalidAmountRequest)?
-                .into()),
-            Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
-            Some(MeltOptions::Amountless { amountless }) => {
-                let amount = amountless.amount_msat;
-                if let Some(amount_msat) = request.amount_milli_satoshis() {
-                    if amount != amount_msat.into() {
-                        return Err(Error::InvalidAmountRequest);
-                    }
-                }
-                Ok(amount)
-            }
-        }
-    }
-}
-
 /// Possible states of a quote
 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
 #[serde(rename_all = "UPPERCASE")]
@@ -184,177 +75,11 @@ impl FromStr for QuoteState {
     }
 }
 
-/// Melt quote response [NUT-05]
-#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
-#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
-#[serde(bound = "Q: Serialize")]
-pub struct MeltQuoteBolt11Response<Q> {
-    /// Quote Id
-    pub quote: Q,
-    /// The amount that needs to be provided
-    pub amount: Amount,
-    /// The fee reserve that is required
-    pub fee_reserve: Amount,
-    /// Whether the request haas be paid
-    // TODO: To be deprecated
-    /// Deprecated
-    pub paid: Option<bool>,
-    /// Quote State
-    pub state: MeltQuoteState,
-    /// Unix timestamp until the quote is valid
-    pub expiry: u64,
-    /// Payment preimage
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub payment_preimage: Option<String>,
-    /// Change
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub change: Option<Vec<BlindSignature>>,
-    /// Payment request to fulfill
-    // REVIEW: This is now required in the spec, we should remove the option once all mints update
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub request: Option<String>,
-    /// Unit
-    // REVIEW: This is now required in the spec, we should remove the option once all mints update
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub unit: Option<CurrencyUnit>,
-}
-
-impl<Q: ToString> MeltQuoteBolt11Response<Q> {
-    /// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a
-    /// `MeltQuoteBolt11Response` with `String`
-    pub fn to_string_id(self) -> MeltQuoteBolt11Response<String> {
-        MeltQuoteBolt11Response {
-            quote: self.quote.to_string(),
-            amount: self.amount,
-            fee_reserve: self.fee_reserve,
-            paid: self.paid,
-            state: self.state,
-            expiry: self.expiry,
-            payment_preimage: self.payment_preimage,
-            change: self.change,
-            request: self.request,
-            unit: self.unit,
-        }
-    }
-}
-
-#[cfg(feature = "mint")]
-impl From<MeltQuoteBolt11Response<Uuid>> for MeltQuoteBolt11Response<String> {
-    fn from(value: MeltQuoteBolt11Response<Uuid>) -> Self {
-        Self {
-            quote: value.quote.to_string(),
-            amount: value.amount,
-            fee_reserve: value.fee_reserve,
-            paid: value.paid,
-            state: value.state,
-            expiry: value.expiry,
-            payment_preimage: value.payment_preimage,
-            change: value.change,
-            request: value.request,
-            unit: value.unit,
-        }
-    }
-}
-
-// A custom deserializer is needed until all mints
-// update some will return without the required state.
-impl<'de, Q: DeserializeOwned> Deserialize<'de> for MeltQuoteBolt11Response<Q> {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        let value = Value::deserialize(deserializer)?;
-
-        let quote: Q = serde_json::from_value(
-            value
-                .get("quote")
-                .ok_or(serde::de::Error::missing_field("quote"))?
-                .clone(),
-        )
-        .map_err(|_| serde::de::Error::custom("Invalid quote if string"))?;
-
-        let amount = value
-            .get("amount")
-            .ok_or(serde::de::Error::missing_field("amount"))?
-            .as_u64()
-            .ok_or(serde::de::Error::missing_field("amount"))?;
-        let amount = Amount::from(amount);
-
-        let fee_reserve = value
-            .get("fee_reserve")
-            .ok_or(serde::de::Error::missing_field("fee_reserve"))?
-            .as_u64()
-            .ok_or(serde::de::Error::missing_field("fee_reserve"))?;
-
-        let fee_reserve = Amount::from(fee_reserve);
-
-        let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
-
-        let state: Option<String> = value
-            .get("state")
-            .and_then(|s| serde_json::from_value(s.clone()).ok());
-
-        let (state, paid) = match (state, paid) {
-            (None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
-            (Some(state), _) => {
-                let state: QuoteState = QuoteState::from_str(&state)
-                    .map_err(|_| serde::de::Error::custom("Unknown state"))?;
-                let paid = state == QuoteState::Paid;
-
-                (state, paid)
-            }
-            (None, Some(paid)) => {
-                let state = if paid {
-                    QuoteState::Paid
-                } else {
-                    QuoteState::Unpaid
-                };
-                (state, paid)
-            }
-        };
-
-        let expiry = value
-            .get("expiry")
-            .ok_or(serde::de::Error::missing_field("expiry"))?
-            .as_u64()
-            .ok_or(serde::de::Error::missing_field("expiry"))?;
-
-        let payment_preimage: Option<String> = value
-            .get("payment_preimage")
-            .and_then(|p| serde_json::from_value(p.clone()).ok());
-
-        let change: Option<Vec<BlindSignature>> = value
-            .get("change")
-            .and_then(|b| serde_json::from_value(b.clone()).ok());
-
-        let request: Option<String> = value
-            .get("request")
-            .and_then(|r| serde_json::from_value(r.clone()).ok());
-
-        let unit: Option<CurrencyUnit> = value
-            .get("unit")
-            .and_then(|u| serde_json::from_value(u.clone()).ok());
-
-        Ok(Self {
-            quote,
-            amount,
-            fee_reserve,
-            paid: Some(paid),
-            state,
-            expiry,
-            payment_preimage,
-            change,
-            request,
-            unit,
-        })
-    }
-}
-
 /// Melt Bolt11 Request [NUT-05]
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 #[serde(bound = "Q: Serialize + DeserializeOwned")]
-pub struct MeltBolt11Request<Q> {
+pub struct MeltRequest<Q> {
     /// Quote ID
     quote: Q,
     /// Proofs
@@ -366,10 +91,10 @@ pub struct MeltBolt11Request<Q> {
 }
 
 #[cfg(feature = "mint")]
-impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
+impl TryFrom<MeltRequest<String>> for MeltRequest<Uuid> {
     type Error = uuid::Error;
 
-    fn try_from(value: MeltBolt11Request<String>) -> Result<Self, Self::Error> {
+    fn try_from(value: MeltRequest<String>) -> Result<Self, Self::Error> {
         Ok(Self {
             quote: Uuid::from_str(&value.quote)?,
             inputs: value.inputs,
@@ -379,7 +104,7 @@ impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
 }
 
 // Basic implementation without trait bounds
-impl<Q> MeltBolt11Request<Q> {
+impl<Q> MeltRequest<Q> {
     /// Get inputs (proofs)
     pub fn inputs(&self) -> &Proofs {
         &self.inputs
@@ -391,8 +116,8 @@ impl<Q> MeltBolt11Request<Q> {
     }
 }
 
-impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
-    /// Create new [`MeltBolt11Request`]
+impl<Q: Serialize + DeserializeOwned> MeltRequest<Q> {
+    /// Create new [`MeltRequest`]
     pub fn new(quote: Q, inputs: Proofs, outputs: Option<Vec<BlindedMessage>>) -> Self {
         Self {
             quote,
@@ -414,7 +139,7 @@ impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
 }
 
 /// Melt Method Settings
-#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
 pub struct MeltMethodSettings {
     /// Payment Method e.g. bolt11
@@ -422,14 +147,168 @@ pub struct MeltMethodSettings {
     /// Currency Unit e.g. sat
     pub unit: CurrencyUnit,
     /// Min Amount
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub min_amount: Option<Amount>,
     /// Max Amount
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub max_amount: Option<Amount>,
-    /// Amountless
-    #[serde(default)]
-    pub amountless: bool,
+    /// Options
+    pub options: Option<MeltMethodOptions>,
+}
+
+impl Serialize for MeltMethodSettings {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut num_fields = 3; // method and unit are always present
+        if self.min_amount.is_some() {
+            num_fields += 1;
+        }
+        if self.max_amount.is_some() {
+            num_fields += 1;
+        }
+
+        let mut amountless_in_top_level = false;
+        if let Some(MeltMethodOptions::Bolt11 { amountless }) = &self.options {
+            if *amountless {
+                num_fields += 1;
+                amountless_in_top_level = true;
+            }
+        }
+
+        let mut state = serializer.serialize_struct("MeltMethodSettings", num_fields)?;
+
+        state.serialize_field("method", &self.method)?;
+        state.serialize_field("unit", &self.unit)?;
+
+        if let Some(min_amount) = &self.min_amount {
+            state.serialize_field("min_amount", min_amount)?;
+        }
+
+        if let Some(max_amount) = &self.max_amount {
+            state.serialize_field("max_amount", max_amount)?;
+        }
+
+        // If there's an amountless flag in Bolt11 options, add it at the top level
+        if amountless_in_top_level {
+            state.serialize_field("amountless", &true)?;
+        }
+
+        state.end()
+    }
+}
+
+struct MeltMethodSettingsVisitor;
+
+impl<'de> Visitor<'de> for MeltMethodSettingsVisitor {
+    type Value = MeltMethodSettings;
+
+    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        formatter.write_str("a MeltMethodSettings structure")
+    }
+
+    fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+    where
+        M: MapAccess<'de>,
+    {
+        let mut method: Option<PaymentMethod> = None;
+        let mut unit: Option<CurrencyUnit> = None;
+        let mut min_amount: Option<Amount> = None;
+        let mut max_amount: Option<Amount> = None;
+        let mut amountless: Option<bool> = None;
+
+        while let Some(key) = map.next_key::<String>()? {
+            match key.as_str() {
+                "method" => {
+                    if method.is_some() {
+                        return Err(de::Error::duplicate_field("method"));
+                    }
+                    method = Some(map.next_value()?);
+                }
+                "unit" => {
+                    if unit.is_some() {
+                        return Err(de::Error::duplicate_field("unit"));
+                    }
+                    unit = Some(map.next_value()?);
+                }
+                "min_amount" => {
+                    if min_amount.is_some() {
+                        return Err(de::Error::duplicate_field("min_amount"));
+                    }
+                    min_amount = Some(map.next_value()?);
+                }
+                "max_amount" => {
+                    if max_amount.is_some() {
+                        return Err(de::Error::duplicate_field("max_amount"));
+                    }
+                    max_amount = Some(map.next_value()?);
+                }
+                "amountless" => {
+                    if amountless.is_some() {
+                        return Err(de::Error::duplicate_field("amountless"));
+                    }
+                    amountless = Some(map.next_value()?);
+                }
+                "options" => {
+                    // If there are explicit options, they take precedence, except the amountless
+                    // field which we will handle specially
+                    let options: Option<MeltMethodOptions> = map.next_value()?;
+
+                    if let Some(MeltMethodOptions::Bolt11 {
+                        amountless: amountless_from_options,
+                    }) = options
+                    {
+                        // If we already found a top-level amountless, use that instead
+                        if amountless.is_none() {
+                            amountless = Some(amountless_from_options);
+                        }
+                    }
+                }
+                _ => {
+                    // Skip unknown fields
+                    let _: serde::de::IgnoredAny = map.next_value()?;
+                }
+            }
+        }
+
+        let method = method.ok_or_else(|| de::Error::missing_field("method"))?;
+        let unit = unit.ok_or_else(|| de::Error::missing_field("unit"))?;
+
+        // Create options based on the method and the amountless flag
+        let options = if method == PaymentMethod::Bolt11 && amountless.is_some() {
+            amountless.map(|amountless| MeltMethodOptions::Bolt11 { amountless })
+        } else {
+            None
+        };
+
+        Ok(MeltMethodSettings {
+            method,
+            unit,
+            min_amount,
+            max_amount,
+            options,
+        })
+    }
+}
+
+impl<'de> Deserialize<'de> for MeltMethodSettings {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        deserializer.deserialize_map(MeltMethodSettingsVisitor)
+    }
+}
+
+/// Mint Method settings options
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(untagged)]
+pub enum MeltMethodOptions {
+    /// Bolt11 Options
+    Bolt11 {
+        /// Mint supports paying bolt11 amountless
+        amountless: bool,
+    },
 }
 
 impl Settings {
@@ -475,3 +354,73 @@ pub struct Settings {
     /// Minting disabled
     pub disabled: bool,
 }
+
+#[cfg(test)]
+mod tests {
+    use serde_json::{from_str, json, to_string};
+
+    use super::*;
+
+    #[test]
+    fn test_melt_method_settings_top_level_amountless() {
+        // Create JSON with top-level amountless
+        let json_str = r#"{
+            "method": "bolt11",
+            "unit": "sat",
+            "min_amount": 0,
+            "max_amount": 10000,
+            "amountless": true
+        }"#;
+
+        // Deserialize it
+        let settings: MeltMethodSettings = from_str(json_str).unwrap();
+
+        // Check that amountless was correctly moved to options
+        assert_eq!(settings.method, PaymentMethod::Bolt11);
+        assert_eq!(settings.unit, CurrencyUnit::Sat);
+        assert_eq!(settings.min_amount, Some(Amount::from(0)));
+        assert_eq!(settings.max_amount, Some(Amount::from(10000)));
+
+        match settings.options {
+            Some(MeltMethodOptions::Bolt11 { amountless }) => {
+                assert_eq!(amountless, true);
+            }
+            _ => panic!("Expected Bolt11 options with amountless = true"),
+        }
+
+        // Serialize it back
+        let serialized = to_string(&settings).unwrap();
+        let parsed: serde_json::Value = from_str(&serialized).unwrap();
+
+        // Verify the amountless is at the top level
+        assert_eq!(parsed["amountless"], json!(true));
+    }
+
+    #[test]
+    fn test_both_amountless_locations() {
+        // Create JSON with amountless in both places (top level and in options)
+        let json_str = r#"{
+            "method": "bolt11",
+            "unit": "sat",
+            "min_amount": 0,
+            "max_amount": 10000,
+            "amountless": true,
+            "options": {
+                "amountless": false
+            }
+        }"#;
+
+        // Deserialize it - top level should take precedence
+        let settings: MeltMethodSettings = from_str(json_str).unwrap();
+
+        match settings.options {
+            Some(MeltMethodOptions::Bolt11 { amountless }) => {
+                assert_eq!(
+                    amountless, true,
+                    "Top-level amountless should take precedence"
+                );
+            }
+            _ => panic!("Expected Bolt11 options with amountless = true"),
+        }
+    }
+}

+ 17 - 2
crates/cashu/src/nuts/nut06.rs

@@ -470,6 +470,7 @@ impl ContactInfo {
 mod tests {
 
     use super::*;
+    use crate::nut04::MintMethodOptions;
 
     #[test]
     fn test_des_mint_into() {
@@ -552,7 +553,9 @@ mod tests {
         "unit": "sat",
         "min_amount": 0,
         "max_amount": 10000,
-        "description": true
+        "options": {
+            "description": true
+            }
         }
       ],
       "disabled": false
@@ -598,7 +601,9 @@ mod tests {
                 "unit": "sat",
                 "min_amount": 0,
                 "max_amount": 10000,
-                "description": true
+                "options": {
+                     "description": true
+                 }
                 }
             ],
             "disabled": false
@@ -624,6 +629,16 @@ mod tests {
 }"#;
         let mint_info: MintInfo = serde_json::from_str(mint_info_str).unwrap();
 
+        let t = mint_info
+            .nuts
+            .nut04
+            .get_settings(&crate::CurrencyUnit::Sat, &crate::PaymentMethod::Bolt11)
+            .unwrap();
+
+        let t = t.options.unwrap();
+
+        matches!(t, MintMethodOptions::Bolt11 { description: true });
+
         assert_eq!(info, mint_info);
     }
 }

+ 3 - 2
crates/cashu/src/nuts/nut08.rs

@@ -2,10 +2,11 @@
 //!
 //! <https://github.com/cashubtc/nuts/blob/main/08.md>
 
-use super::nut05::{MeltBolt11Request, MeltQuoteBolt11Response};
+use super::nut05::MeltRequest;
+use super::nut23::MeltQuoteBolt11Response;
 use crate::Amount;
 
-impl<Q> MeltBolt11Request<Q> {
+impl<Q> MeltRequest<Q> {
     /// Total output [`Amount`]
     pub fn output_amount(&self) -> Option<Amount> {
         self.outputs()

+ 8 - 8
crates/cashu/src/nuts/nut20.rs

@@ -5,7 +5,7 @@ use std::str::FromStr;
 use bitcoin::secp256k1::schnorr::Signature;
 use thiserror::Error;
 
-use super::{MintBolt11Request, PublicKey, SecretKey};
+use super::{MintRequest, PublicKey, SecretKey};
 
 /// Nut19 Error
 #[derive(Debug, Error)]
@@ -21,7 +21,7 @@ pub enum Error {
     NUT01(#[from] crate::nuts::nut01::Error),
 }
 
-impl<Q> MintBolt11Request<Q>
+impl<Q> MintRequest<Q>
 where
     Q: ToString,
 {
@@ -46,7 +46,7 @@ where
         msg
     }
 
-    /// Sign [`MintBolt11Request`]
+    /// Sign [`MintRequest`]
     pub fn sign(&mut self, secret_key: SecretKey) -> Result<(), Error> {
         let msg = self.msg_to_sign();
 
@@ -57,7 +57,7 @@ where
         Ok(())
     }
 
-    /// Verify signature on [`MintBolt11Request`]
+    /// Verify signature on [`MintRequest`]
     pub fn verify_signature(&self, pubkey: PublicKey) -> Result<(), Error> {
         let signature = self.signature.as_ref().ok_or(Error::SignatureMissing)?;
 
@@ -80,7 +80,7 @@ mod tests {
 
     #[test]
     fn test_msg_to_sign() {
-        let request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
+        let request: MintRequest<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
 
         // let expected_msg_to_sign = "9d745270-1405-46de-b5c5-e2762b4f5e000342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c31102be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b5302209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79";
 
@@ -118,14 +118,14 @@ mod tests {
         )
         .unwrap();
 
-        let request: MintBolt11Request<Uuid> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}], "signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"}"#).unwrap();
+        let request: MintRequest<Uuid> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}], "signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0"}"#).unwrap();
 
         assert!(request.verify_signature(pubkey).is_ok());
     }
 
     #[test]
     fn test_mint_request_signature() {
-        let mut request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}]}"#).unwrap();
+        let mut request: MintRequest<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}]}"#).unwrap();
 
         let secret =
             SecretKey::from_hex("50d7fd7aa2b2fe4607f41f4ce6f8794fc184dd47b8cdfbe4b3d1249aa02d35aa")
@@ -143,7 +143,7 @@ mod tests {
         )
         .unwrap();
 
-        let request: MintBolt11Request<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
+        let request: MintRequest<String> = serde_json::from_str(r#"{"quote":"9d745270-1405-46de-b5c5-e2762b4f5e00","outputs":[{"amount":1,"id":"00456a94ab4e1c46","B_":"0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834"},{"amount":1,"id":"00456a94ab4e1c46","B_":"032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4"},{"amount":1,"id":"00456a94ab4e1c46","B_":"033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53"},{"amount":1,"id":"00456a94ab4e1c46","B_":"02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79"}],"signature":"cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3"}"#).unwrap();
 
         // Signature is on a different quote id verification should fail
         assert!(request.verify_signature(pubkey).is_err());

+ 411 - 0
crates/cashu/src/nuts/nut23.rs

@@ -0,0 +1,411 @@
+//! Bolt11
+
+use std::fmt;
+use std::str::FromStr;
+
+use lightning_invoice::Bolt11Invoice;
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Deserializer, Serialize};
+use serde_json::Value;
+use thiserror::Error;
+#[cfg(feature = "mint")]
+use uuid::Uuid;
+
+use super::{BlindSignature, CurrencyUnit, MeltQuoteState, Mpp, PublicKey};
+use crate::Amount;
+
+/// NUT023 Error
+#[derive(Debug, Error)]
+pub enum Error {
+    /// Unknown Quote State
+    #[error("Unknown Quote State")]
+    UnknownState,
+    /// Amount overflow
+    #[error("Amount overflow")]
+    AmountOverflow,
+    /// Invalid Amount
+    #[error("Invalid Request")]
+    InvalidAmountRequest,
+}
+
+/// Mint quote request [NUT-04]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MintQuoteBolt11Request {
+    /// Amount
+    pub amount: Amount,
+    /// Unit wallet would like to pay with
+    pub unit: CurrencyUnit,
+    /// Memo to create the invoice with
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub description: Option<String>,
+    /// NUT-19 Pubkey
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pubkey: Option<PublicKey>,
+}
+
+/// Possible states of a quote
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
+#[serde(rename_all = "UPPERCASE")]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MintQuoteState))]
+pub enum QuoteState {
+    /// Quote has not been paid
+    #[default]
+    Unpaid,
+    /// Quote has been paid and wallet can mint
+    Paid,
+    /// Minting is in progress
+    /// **Note:** This state is to be used internally but is not part of the
+    /// nut.
+    Pending,
+    /// ecash issued for quote
+    Issued,
+}
+
+impl fmt::Display for QuoteState {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            Self::Unpaid => write!(f, "UNPAID"),
+            Self::Paid => write!(f, "PAID"),
+            Self::Pending => write!(f, "PENDING"),
+            Self::Issued => write!(f, "ISSUED"),
+        }
+    }
+}
+
+impl FromStr for QuoteState {
+    type Err = Error;
+
+    fn from_str(state: &str) -> Result<Self, Self::Err> {
+        match state {
+            "PENDING" => Ok(Self::Pending),
+            "PAID" => Ok(Self::Paid),
+            "UNPAID" => Ok(Self::Unpaid),
+            "ISSUED" => Ok(Self::Issued),
+            _ => Err(Error::UnknownState),
+        }
+    }
+}
+
+/// Mint quote response [NUT-04]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(bound = "Q: Serialize + DeserializeOwned")]
+pub struct MintQuoteBolt11Response<Q> {
+    /// Quote Id
+    pub quote: Q,
+    /// Payment request to fulfil
+    pub request: String,
+    /// Amount
+    // REVIEW: This is now required in the spec, we should remove the option once all mints update
+    pub amount: Option<Amount>,
+    /// Unit
+    // REVIEW: This is now required in the spec, we should remove the option once all mints update
+    pub unit: Option<CurrencyUnit>,
+    /// Quote State
+    pub state: QuoteState,
+    /// Unix timestamp until the quote is valid
+    pub expiry: Option<u64>,
+    /// NUT-19 Pubkey
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub pubkey: Option<PublicKey>,
+}
+impl<Q: ToString> MintQuoteBolt11Response<Q> {
+    /// Convert the MintQuote with a quote type Q to a String
+    pub fn to_string_id(&self) -> MintQuoteBolt11Response<String> {
+        MintQuoteBolt11Response {
+            quote: self.quote.to_string(),
+            request: self.request.clone(),
+            state: self.state,
+            expiry: self.expiry,
+            pubkey: self.pubkey,
+            amount: self.amount,
+            unit: self.unit.clone(),
+        }
+    }
+}
+
+#[cfg(feature = "mint")]
+impl From<MintQuoteBolt11Response<Uuid>> for MintQuoteBolt11Response<String> {
+    fn from(value: MintQuoteBolt11Response<Uuid>) -> Self {
+        Self {
+            quote: value.quote.to_string(),
+            request: value.request,
+            state: value.state,
+            expiry: value.expiry,
+            pubkey: value.pubkey,
+            amount: value.amount,
+            unit: value.unit.clone(),
+        }
+    }
+}
+
+/// BOLT11 melt quote request [NUT-23]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct MeltQuoteBolt11Request {
+    /// Bolt11 invoice to be paid
+    #[cfg_attr(feature = "swagger", schema(value_type = String))]
+    pub request: Bolt11Invoice,
+    /// Unit wallet would like to pay with
+    pub unit: CurrencyUnit,
+    /// Payment Options
+    pub options: Option<MeltOptions>,
+}
+
+/// Melt Options
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(untagged)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub enum MeltOptions {
+    /// Mpp Options
+    Mpp {
+        /// MPP
+        mpp: Mpp,
+    },
+    /// Amountless options
+    Amountless {
+        /// Amountless
+        amountless: Amountless,
+    },
+}
+
+impl MeltOptions {
+    /// Create new [`MeltOptions::Mpp`]
+    pub fn new_mpp<A>(amount: A) -> Self
+    where
+        A: Into<Amount>,
+    {
+        Self::Mpp {
+            mpp: Mpp {
+                amount: amount.into(),
+            },
+        }
+    }
+
+    /// Create new [`MeltOptions::Amountless`]
+    pub fn new_amountless<A>(amount_msat: A) -> Self
+    where
+        A: Into<Amount>,
+    {
+        Self::Amountless {
+            amountless: Amountless {
+                amount_msat: amount_msat.into(),
+            },
+        }
+    }
+
+    /// Payment amount
+    pub fn amount_msat(&self) -> Amount {
+        match self {
+            Self::Mpp { mpp } => mpp.amount,
+            Self::Amountless { amountless } => amountless.amount_msat,
+        }
+    }
+}
+
+/// Amountless payment
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+pub struct Amountless {
+    /// Amount to pay in msat
+    pub amount_msat: Amount,
+}
+
+impl MeltQuoteBolt11Request {
+    /// Amount from [`MeltQuoteBolt11Request`]
+    ///
+    /// Amount can either be defined in the bolt11 invoice,
+    /// in the request for an amountless bolt11 or in MPP option.
+    pub fn amount_msat(&self) -> Result<Amount, Error> {
+        let MeltQuoteBolt11Request {
+            request,
+            unit: _,
+            options,
+            ..
+        } = self;
+
+        match options {
+            None => Ok(request
+                .amount_milli_satoshis()
+                .ok_or(Error::InvalidAmountRequest)?
+                .into()),
+            Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
+            Some(MeltOptions::Amountless { amountless }) => {
+                let amount = amountless.amount_msat;
+                if let Some(amount_msat) = request.amount_milli_satoshis() {
+                    if amount != amount_msat.into() {
+                        return Err(Error::InvalidAmountRequest);
+                    }
+                }
+                Ok(amount)
+            }
+        }
+    }
+}
+
+/// Melt quote response [NUT-05]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
+#[serde(bound = "Q: Serialize")]
+pub struct MeltQuoteBolt11Response<Q> {
+    /// Quote Id
+    pub quote: Q,
+    /// The amount that needs to be provided
+    pub amount: Amount,
+    /// The fee reserve that is required
+    pub fee_reserve: Amount,
+    /// Whether the request haas be paid
+    // TODO: To be deprecated
+    /// Deprecated
+    pub paid: Option<bool>,
+    /// Quote State
+    pub state: MeltQuoteState,
+    /// Unix timestamp until the quote is valid
+    pub expiry: u64,
+    /// Payment preimage
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub payment_preimage: Option<String>,
+    /// Change
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub change: Option<Vec<BlindSignature>>,
+    /// Payment request to fulfill
+    // REVIEW: This is now required in the spec, we should remove the option once all mints update
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub request: Option<String>,
+    /// Unit
+    // REVIEW: This is now required in the spec, we should remove the option once all mints update
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub unit: Option<CurrencyUnit>,
+}
+
+impl<Q: ToString> MeltQuoteBolt11Response<Q> {
+    /// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a
+    /// `MeltQuoteBolt11Response` with `String`
+    pub fn to_string_id(self) -> MeltQuoteBolt11Response<String> {
+        MeltQuoteBolt11Response {
+            quote: self.quote.to_string(),
+            amount: self.amount,
+            fee_reserve: self.fee_reserve,
+            paid: self.paid,
+            state: self.state,
+            expiry: self.expiry,
+            payment_preimage: self.payment_preimage,
+            change: self.change,
+            request: self.request,
+            unit: self.unit,
+        }
+    }
+}
+
+#[cfg(feature = "mint")]
+impl From<MeltQuoteBolt11Response<Uuid>> for MeltQuoteBolt11Response<String> {
+    fn from(value: MeltQuoteBolt11Response<Uuid>) -> Self {
+        Self {
+            quote: value.quote.to_string(),
+            amount: value.amount,
+            fee_reserve: value.fee_reserve,
+            paid: value.paid,
+            state: value.state,
+            expiry: value.expiry,
+            payment_preimage: value.payment_preimage,
+            change: value.change,
+            request: value.request,
+            unit: value.unit,
+        }
+    }
+}
+
+// A custom deserializer is needed until all mints
+// update some will return without the required state.
+impl<'de, Q: DeserializeOwned> Deserialize<'de> for MeltQuoteBolt11Response<Q> {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        let value = Value::deserialize(deserializer)?;
+
+        let quote: Q = serde_json::from_value(
+            value
+                .get("quote")
+                .ok_or(serde::de::Error::missing_field("quote"))?
+                .clone(),
+        )
+        .map_err(|_| serde::de::Error::custom("Invalid quote if string"))?;
+
+        let amount = value
+            .get("amount")
+            .ok_or(serde::de::Error::missing_field("amount"))?
+            .as_u64()
+            .ok_or(serde::de::Error::missing_field("amount"))?;
+        let amount = Amount::from(amount);
+
+        let fee_reserve = value
+            .get("fee_reserve")
+            .ok_or(serde::de::Error::missing_field("fee_reserve"))?
+            .as_u64()
+            .ok_or(serde::de::Error::missing_field("fee_reserve"))?;
+
+        let fee_reserve = Amount::from(fee_reserve);
+
+        let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
+
+        let state: Option<String> = value
+            .get("state")
+            .and_then(|s| serde_json::from_value(s.clone()).ok());
+
+        let (state, paid) = match (state, paid) {
+            (None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
+            (Some(state), _) => {
+                let state: MeltQuoteState = MeltQuoteState::from_str(&state)
+                    .map_err(|_| serde::de::Error::custom("Unknown state"))?;
+                let paid = state == MeltQuoteState::Paid;
+
+                (state, paid)
+            }
+            (None, Some(paid)) => {
+                let state = if paid {
+                    MeltQuoteState::Paid
+                } else {
+                    MeltQuoteState::Unpaid
+                };
+                (state, paid)
+            }
+        };
+
+        let expiry = value
+            .get("expiry")
+            .ok_or(serde::de::Error::missing_field("expiry"))?
+            .as_u64()
+            .ok_or(serde::de::Error::missing_field("expiry"))?;
+
+        let payment_preimage: Option<String> = value
+            .get("payment_preimage")
+            .and_then(|p| serde_json::from_value(p.clone()).ok());
+
+        let change: Option<Vec<BlindSignature>> = value
+            .get("change")
+            .and_then(|b| serde_json::from_value(b.clone()).ok());
+
+        let request: Option<String> = value
+            .get("request")
+            .and_then(|r| serde_json::from_value(r.clone()).ok());
+
+        let unit: Option<CurrencyUnit> = value
+            .get("unit")
+            .and_then(|u| serde_json::from_value(u.clone()).ok());
+
+        Ok(Self {
+            quote,
+            amount,
+            fee_reserve,
+            paid: Some(paid),
+            state,
+            expiry,
+            payment_preimage,
+            change,
+            request,
+            unit,
+        })
+    }
+}

+ 3 - 3
crates/cdk-axum/src/auth.rs

@@ -9,7 +9,7 @@ use axum::{Json, Router};
 #[cfg(feature = "swagger")]
 use cdk::error::ErrorResponse;
 use cdk::nuts::{
-    AuthToken, BlindAuthToken, KeysResponse, KeysetResponse, MintAuthRequest, MintBolt11Response,
+    AuthToken, BlindAuthToken, KeysResponse, KeysetResponse, MintAuthRequest, MintResponse,
 };
 use serde::{Deserialize, Serialize};
 
@@ -144,7 +144,7 @@ pub async fn get_blind_auth_keys(
     path = "/blind/mint",
     request_body(content = MintAuthRequest, description = "Request params", content_type = "application/json"),
     responses(
-        (status = 200, description = "Successful response", body = MintBolt11Response, content_type = "application/json"),
+        (status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
         (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
     )
 ))]
@@ -152,7 +152,7 @@ pub async fn post_mint_auth(
     auth: AuthHeader,
     State(state): State<MintState>,
     Json(payload): Json<MintAuthRequest>,
-) -> Result<Json<MintBolt11Response>, Response> {
+) -> Result<Json<MintResponse>, Response> {
     let auth_token = match auth {
         AuthHeader::Clear(cat) => {
             if cat.is_empty() {

+ 9 - 10
crates/cdk-axum/src/lib.rs

@@ -33,13 +33,8 @@ mod swagger_imports {
     pub use cdk::nuts::nut01::{Keys, KeysResponse, PublicKey, SecretKey};
     pub use cdk::nuts::nut02::{KeySet, KeySetInfo, KeysetResponse};
     pub use cdk::nuts::nut03::{SwapRequest, SwapResponse};
-    pub use cdk::nuts::nut04::{
-        MintBolt11Request, MintBolt11Response, MintMethodSettings, MintQuoteBolt11Request,
-        MintQuoteBolt11Response,
-    };
-    pub use cdk::nuts::nut05::{
-        MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
-    };
+    pub use cdk::nuts::nut04::{MintMethodSettings, MintRequest, MintResponse};
+    pub use cdk::nuts::nut05::{MeltMethodSettings, MeltRequest};
     pub use cdk::nuts::nut06::{ContactInfo, MintInfo, MintVersion, Nuts, SupportedSettings};
     pub use cdk::nuts::nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};
     pub use cdk::nuts::nut09::{RestoreRequest, RestoreResponse};
@@ -47,6 +42,10 @@ mod swagger_imports {
     pub use cdk::nuts::nut12::{BlindSignatureDleq, ProofDleq};
     pub use cdk::nuts::nut14::HTLCWitness;
     pub use cdk::nuts::nut15::{Mpp, MppMethodSettings};
+    pub use cdk::nuts::nut23::{
+        MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintQuoteBolt11Request,
+        MintQuoteBolt11Response,
+    };
     pub use cdk::nuts::{nut04, nut05, nut15, MeltQuoteState, MintQuoteState};
 }
 
@@ -80,13 +79,13 @@ pub struct MintState {
         KeysetResponse,
         KeySet,
         KeySetInfo,
-        MeltBolt11Request<String>,
+        MeltRequest<String>,
         MeltQuoteBolt11Request,
         MeltQuoteBolt11Response<String>,
         MeltQuoteState,
         MeltMethodSettings,
-        MintBolt11Request<String>,
-        MintBolt11Response,
+        MintRequest<String>,
+        MintResponse,
         MintInfo,
         MintQuoteBolt11Request,
         MintQuoteBolt11Response<String>,

+ 11 - 15
crates/cdk-axum/src/router_handlers.rs

@@ -7,9 +7,9 @@ use cdk::error::ErrorResponse;
 #[cfg(feature = "auth")]
 use cdk::nuts::nut21::{Method, ProtectedEndpoint, RoutePath};
 use cdk::nuts::{
-    CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse, MeltBolt11Request,
-    MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response,
-    MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, RestoreResponse,
+    CheckStateRequest, CheckStateResponse, Id, KeysResponse, KeysetResponse,
+    MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
+    MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
     SwapRequest, SwapResponse,
 };
 use cdk::util::unix_time;
@@ -60,14 +60,10 @@ macro_rules! post_cache_wrapper {
 }
 
 post_cache_wrapper!(post_swap, SwapRequest, SwapResponse);
-post_cache_wrapper!(
-    post_mint_bolt11,
-    MintBolt11Request<Uuid>,
-    MintBolt11Response
-);
+post_cache_wrapper!(post_mint_bolt11, MintRequest<Uuid>, MintResponse);
 post_cache_wrapper!(
     post_melt_bolt11,
-    MeltBolt11Request<Uuid>,
+    MeltRequest<Uuid>,
     MeltQuoteBolt11Response<Uuid>
 );
 
@@ -246,9 +242,9 @@ pub(crate) async fn ws_handler(
     post,
     context_path = "/v1",
     path = "/mint/bolt11",
-    request_body(content = MintBolt11Request<String>, description = "Request params", content_type = "application/json"),
+    request_body(content = MintRequest<String>, description = "Request params", content_type = "application/json"),
     responses(
-        (status = 200, description = "Successful response", body = MintBolt11Response, content_type = "application/json"),
+        (status = 200, description = "Successful response", body = MintResponse, content_type = "application/json"),
         (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
     )
 ))]
@@ -256,8 +252,8 @@ pub(crate) async fn ws_handler(
 pub(crate) async fn post_mint_bolt11(
     #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
-    Json(payload): Json<MintBolt11Request<Uuid>>,
-) -> Result<Json<MintBolt11Response>, Response> {
+    Json(payload): Json<MintRequest<Uuid>>,
+) -> Result<Json<MintResponse>, Response> {
     #[cfg(feature = "auth")]
     {
         state
@@ -369,7 +365,7 @@ pub(crate) async fn get_check_melt_bolt11_quote(
     post,
     context_path = "/v1",
     path = "/melt/bolt11",
-    request_body(content = MeltBolt11Request<String>, description = "Melt params", content_type = "application/json"),
+    request_body(content = MeltRequest<String>, description = "Melt params", content_type = "application/json"),
     responses(
         (status = 200, description = "Successful response", body = MeltQuoteBolt11Response<String>, content_type = "application/json"),
         (status = 500, description = "Server error", body = ErrorResponse, content_type = "application/json")
@@ -382,7 +378,7 @@ pub(crate) async fn get_check_melt_bolt11_quote(
 pub(crate) async fn post_melt_bolt11(
     #[cfg(feature = "auth")] auth: AuthHeader,
     State(state): State<MintState>,
-    Json(payload): Json<MeltBolt11Request<Uuid>>,
+    Json(payload): Json<MeltRequest<Uuid>>,
 ) -> Result<Json<MeltQuoteBolt11Response<Uuid>>, Response> {
     #[cfg(feature = "auth")]
     {

+ 4 - 4
crates/cdk-common/src/database/mint/mod.rs

@@ -10,8 +10,8 @@ use super::Error;
 use crate::common::{PaymentProcessorKey, QuoteTTL};
 use crate::mint::{self, MintKeySetInfo, MintQuote as MintMintQuote};
 use crate::nuts::{
-    BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof,
-    Proofs, PublicKey, State,
+    BlindSignature, CurrencyUnit, Id, MeltQuoteState, MeltRequest, MintQuoteState, Proof, Proofs,
+    PublicKey, State,
 };
 
 #[cfg(feature = "auth")]
@@ -96,14 +96,14 @@ pub trait QuotesDatabase {
     /// Add melt request
     async fn add_melt_request(
         &self,
-        melt_request: MeltBolt11Request<Uuid>,
+        melt_request: MeltRequest<Uuid>,
         ln_key: PaymentProcessorKey,
     ) -> Result<(), Self::Err>;
     /// Get melt request
     async fn get_melt_request(
         &self,
         quote_id: &Uuid,
-    ) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err>;
+    ) -> Result<Option<(MeltRequest<Uuid>, PaymentProcessorKey)>, Self::Err>;
 }
 
 /// Mint Proof Database trait

+ 5 - 2
crates/cdk-common/src/error.rs

@@ -309,12 +309,15 @@ pub enum Error {
     /// NUT20 Error
     #[error(transparent)]
     NUT20(#[from] crate::nuts::nut20::Error),
-    /// NUTXX Error
+    /// NUT21 Error
     #[error(transparent)]
     NUT21(#[from] crate::nuts::nut21::Error),
-    /// NUTXX1 Error
+    /// NUT22 Error
     #[error(transparent)]
     NUT22(#[from] crate::nuts::nut22::Error),
+    /// NUT23 Error
+    #[error(transparent)]
+    NUT23(#[from] crate::nuts::nut23::Error),
     /// Database Error
     #[error(transparent)]
     Database(crate::database::Error),

+ 3 - 0
crates/cdk-common/src/payment.rs

@@ -52,6 +52,9 @@ pub enum Error {
     /// NUT05 Error
     #[error(transparent)]
     NUT05(#[from] crate::nuts::nut05::Error),
+    /// NUT23 Error
+    #[error(transparent)]
+    NUT23(#[from] crate::nuts::nut23::Error),
     /// Custom
     #[error("`{0}`")]
     Custom(String),

+ 5 - 8
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -14,9 +14,9 @@ use cdk::mint::{MintBuilder, MintMeltLimits};
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{
     CheckStateRequest, CheckStateResponse, CurrencyUnit, Id, KeySet, KeysetResponse,
-    MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
-    MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, PaymentMethod,
-    RestoreRequest, RestoreResponse, SwapRequest, SwapResponse,
+    MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
+    MintQuoteBolt11Response, MintRequest, MintResponse, PaymentMethod, RestoreRequest,
+    RestoreResponse, SwapRequest, SwapResponse,
 };
 use cdk::types::{FeeReserve, QuoteTTL};
 use cdk::util::unix_time;
@@ -91,10 +91,7 @@ impl MintConnector for DirectMintConnection {
             .map(Into::into)
     }
 
-    async fn post_mint(
-        &self,
-        request: MintBolt11Request<String>,
-    ) -> Result<MintBolt11Response, Error> {
+    async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error> {
         let request_uuid = request.try_into().unwrap();
         self.mint.process_mint_request(request_uuid).await
     }
@@ -122,7 +119,7 @@ impl MintConnector for DirectMintConnection {
 
     async fn post_melt(
         &self,
-        request: MeltBolt11Request<String>,
+        request: MeltRequest<String>,
     ) -> Result<MeltQuoteBolt11Response<String>, Error> {
         let request_uuid = request.try_into().unwrap();
         self.mint.melt_bolt11(&request_uuid).await.map(Into::into)

+ 5 - 5
crates/cdk-integration-tests/tests/fake_auth.rs

@@ -8,9 +8,9 @@ use cdk::amount::{Amount, SplitTarget};
 use cdk::mint_url::MintUrl;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{
-    AuthProof, AuthToken, BlindAuthToken, CheckStateRequest, CurrencyUnit, MeltBolt11Request,
-    MeltQuoteBolt11Request, MeltQuoteState, MintBolt11Request, MintQuoteBolt11Request,
-    RestoreRequest, State, SwapRequest,
+    AuthProof, AuthToken, BlindAuthToken, CheckStateRequest, CurrencyUnit, MeltQuoteBolt11Request,
+    MeltQuoteState, MeltRequest, MintQuoteBolt11Request, MintRequest, RestoreRequest, State,
+    SwapRequest,
 };
 use cdk::wallet::{AuthHttpClient, AuthMintConnector, HttpClient, MintConnector, WalletBuilder};
 use cdk::{Error, OidcClient};
@@ -109,7 +109,7 @@ async fn test_mint_without_auth() {
     }
 
     {
-        let request = MintBolt11Request {
+        let request = MintRequest {
             quote: "123e4567-e89b-12d3-a456-426614174000".to_string(),
             outputs: vec![],
             signature: None,
@@ -207,7 +207,7 @@ async fn test_melt_without_auth() {
 
     // Test melt
     {
-        let request = MeltBolt11Request::new(
+        let request = MeltRequest::new(
             "123e4567-e89b-12d3-a456-426614174000".to_string(),
             vec![],
             None,

+ 11 - 11
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -5,8 +5,8 @@ use cashu::Amount;
 use cdk::amount::SplitTarget;
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{
-    CurrencyUnit, MeltBolt11Request, MeltQuoteState, MintBolt11Request, PreMintSecrets, Proofs,
-    SecretKey, State, SwapRequest,
+    CurrencyUnit, MeltQuoteState, MeltRequest, MintRequest, PreMintSecrets, Proofs, SecretKey,
+    State, SwapRequest,
 };
 use cdk::wallet::types::TransactionDirection;
 use cdk::wallet::{HttpClient, MintConnector, Wallet};
@@ -388,7 +388,7 @@ async fn test_fake_melt_change_in_quote() {
 
     let client = HttpClient::new(MINT_URL.parse().unwrap(), None);
 
-    let melt_request = MeltBolt11Request::new(
+    let melt_request = MeltRequest::new(
         melt_quote.id.clone(),
         proofs.clone(),
         Some(premint_secrets.blinded_messages()),
@@ -494,7 +494,7 @@ async fn test_fake_mint_without_witness() {
     let premint_secrets =
         PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
 
-    let request = MintBolt11Request {
+    let request = MintRequest {
         quote: mint_quote.id,
         outputs: premint_secrets.blinded_messages(),
         signature: None,
@@ -534,7 +534,7 @@ async fn test_fake_mint_with_wrong_witness() {
     let premint_secrets =
         PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
 
-    let mut request = MintBolt11Request {
+    let mut request = MintRequest {
         quote: mint_quote.id,
         outputs: premint_secrets.blinded_messages(),
         signature: None,
@@ -585,7 +585,7 @@ async fn test_fake_mint_inflated() {
         .unwrap()
         .expect("there is a quote");
 
-    let mut mint_request = MintBolt11Request {
+    let mut mint_request = MintRequest {
         quote: mint_quote.id,
         outputs: pre_mint.blinded_messages(),
         signature: None,
@@ -662,7 +662,7 @@ async fn test_fake_mint_multiple_units() {
 
     sat_outputs.append(&mut usd_outputs);
 
-    let mut mint_request = MintBolt11Request {
+    let mut mint_request = MintRequest {
         quote: mint_quote.id,
         outputs: sat_outputs,
         signature: None,
@@ -859,7 +859,7 @@ async fn test_fake_mint_multiple_unit_melt() {
         let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string());
         let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
 
-        let melt_request = MeltBolt11Request::new(melt_quote.id, inputs, None);
+        let melt_request = MeltRequest::new(melt_quote.id, inputs, None);
 
         let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
         let response = http_client.post_melt(melt_request.clone()).await;
@@ -901,7 +901,7 @@ async fn test_fake_mint_multiple_unit_melt() {
         usd_outputs.append(&mut sat_outputs);
         let quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
 
-        let melt_request = MeltBolt11Request::new(quote.id, inputs, Some(usd_outputs));
+        let melt_request = MeltRequest::new(quote.id, inputs, Some(usd_outputs));
 
         let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
 
@@ -1148,7 +1148,7 @@ async fn test_fake_mint_melt_spend_after_fail() {
     let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string());
     let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
 
-    let melt_request = MeltBolt11Request::new(melt_quote.id, proofs, None);
+    let melt_request = MeltRequest::new(melt_quote.id, proofs, None);
 
     let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
     let response = http_client.post_melt(melt_request.clone()).await;
@@ -1274,7 +1274,7 @@ async fn test_fake_mint_duplicate_proofs_melt() {
 
     let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
 
-    let melt_request = MeltBolt11Request::new(melt_quote.id, inputs, None);
+    let melt_request = MeltRequest::new(melt_quote.id, inputs, None);
 
     let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None);
     let response = http_client.post_melt(melt_request.clone()).await;

+ 2 - 2
crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs

@@ -16,7 +16,7 @@ use std::time::Duration;
 use std::{char, env};
 
 use bip39::Mnemonic;
-use cashu::{MeltBolt11Request, PreMintSecrets};
+use cashu::{MeltRequest, PreMintSecrets};
 use cdk::amount::{Amount, SplitTarget};
 use cdk::nuts::nut00::ProofsMethods;
 use cdk::nuts::{CurrencyUnit, MeltQuoteState, NotificationPayload, State};
@@ -358,7 +358,7 @@ async fn test_fake_melt_change_in_quote() {
 
     let client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
 
-    let melt_request = MeltBolt11Request::new(
+    let melt_request = MeltRequest::new(
         melt_quote.id.clone(),
         proofs.clone(),
         Some(premint_secrets.blinded_messages()),

+ 3 - 3
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -13,8 +13,8 @@ use cashu::amount::SplitTarget;
 use cashu::dhke::construct_proofs;
 use cashu::mint_url::MintUrl;
 use cashu::{
-    CurrencyUnit, Id, MeltBolt11Request, NotificationPayload, PreMintSecrets, ProofState,
-    SecretKey, SpendingConditions, State, SwapRequest,
+    CurrencyUnit, Id, MeltRequest, NotificationPayload, PreMintSecrets, ProofState, SecretKey,
+    SpendingConditions, State, SwapRequest,
 };
 use cdk::mint::Mint;
 use cdk::nuts::nut00::ProofsMethods;
@@ -855,7 +855,7 @@ async fn test_concurrent_double_spend_melt() {
     let mint_clone2 = mint_bob.clone();
     let mint_clone3 = mint_bob.clone();
 
-    let melt_request = MeltBolt11Request::new(quote_id.parse().unwrap(), proofs.clone(), None);
+    let melt_request = MeltRequest::new(quote_id.parse().unwrap(), proofs.clone(), None);
     let melt_request2 = melt_request.clone();
     let melt_request3 = melt_request.clone();
 

+ 2 - 2
crates/cdk-integration-tests/tests/regtest.rs

@@ -6,7 +6,7 @@ use bip39::Mnemonic;
 use cashu::ProofsMethods;
 use cdk::amount::{Amount, SplitTarget};
 use cdk::nuts::{
-    CurrencyUnit, MeltOptions, MeltQuoteState, MintBolt11Request, MintQuoteState, Mpp,
+    CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState, MintRequest, Mpp,
     NotificationPayload, PreMintSecrets,
 };
 use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription};
@@ -313,7 +313,7 @@ async fn test_cached_mint() {
     let premint_secrets =
         PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
 
-    let mut request = MintBolt11Request {
+    let mut request = MintRequest {
         quote: quote.id,
         outputs: premint_secrets.blinded_messages(),
         signature: None,

+ 9 - 4
crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04.rs

@@ -4,7 +4,7 @@ use tonic::transport::Channel;
 use tonic::Request;
 
 use crate::cdk_mint_client::CdkMintClient;
-use crate::UpdateNut04Request;
+use crate::{MintMethodOptions, UpdateNut04Request};
 
 /// Command to update NUT-04 (mint process) settings for the mint
 ///
@@ -46,14 +46,19 @@ pub async fn update_nut04(
     client: &mut CdkMintClient<Channel>,
     sub_command_args: &UpdateNut04Command,
 ) -> Result<()> {
+    // Create options if description is set
+    let options = sub_command_args
+        .description
+        .map(|description| MintMethodOptions { description });
+
     let _response = client
         .update_nut04(Request::new(UpdateNut04Request {
             method: sub_command_args.method.clone(),
             unit: sub_command_args.unit.clone(),
             disabled: sub_command_args.disabled,
-            min: sub_command_args.min_amount,
-            max: sub_command_args.max_amount,
-            description: sub_command_args.description,
+            min_amount: sub_command_args.min_amount,
+            max_amount: sub_command_args.max_amount,
+            options,
         }))
         .await?;
 

+ 12 - 3
crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut05.rs

@@ -4,7 +4,7 @@ use tonic::transport::Channel;
 use tonic::Request;
 
 use crate::cdk_mint_client::CdkMintClient;
-use crate::UpdateNut05Request;
+use crate::{MeltMethodOptions, UpdateNut05Request};
 
 /// Command to update NUT-05 (melt process) settings for the mint
 ///
@@ -30,6 +30,9 @@ pub struct UpdateNut05Command {
     /// Whether this melt method is disabled (true) or enabled (false)
     #[arg(long)]
     disabled: Option<bool>,
+    /// Whether amountless bolt11 invoices are allowed
+    #[arg(long)]
+    amountless: Option<bool>,
 }
 
 /// Executes the update_nut05 command against the mint server
@@ -43,13 +46,19 @@ pub async fn update_nut05(
     client: &mut CdkMintClient<Channel>,
     sub_command_args: &UpdateNut05Command,
 ) -> Result<()> {
+    // Create options if amountless is set
+    let options = sub_command_args
+        .amountless
+        .map(|amountless| MeltMethodOptions { amountless });
+
     let _response = client
         .update_nut05(Request::new(UpdateNut05Request {
             method: sub_command_args.method.clone(),
             unit: sub_command_args.unit.clone(),
             disabled: sub_command_args.disabled,
-            min: sub_command_args.min_amount,
-            max: sub_command_args.max_amount,
+            min_amount: sub_command_args.min_amount,
+            max_amount: sub_command_args.max_amount,
+            options,
         }))
         .await?;
 

+ 16 - 5
crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto

@@ -72,22 +72,33 @@ message UpdateContactRequest {
     string info = 2;
 }
 
+message MintMethodOptions {
+    // Bolt11 options
+    bool description = 1;
+}
+
 message UpdateNut04Request {
     string unit = 1;
     string method = 2;
     optional bool disabled = 3;
-    optional uint64 min = 4;
-    optional uint64 max = 5;
-    optional bool description = 6;
+    optional uint64 min_amount = 4;
+    optional uint64 max_amount = 5;
+    optional MintMethodOptions options = 6;
 }
 
 
+message MeltMethodOptions {
+    // Bolt11 options
+    bool amountless = 1;
+}
+
 message UpdateNut05Request {
     string unit = 1;
     string method = 2;
     optional bool disabled = 3;
-    optional uint64 min = 4;
-    optional uint64 max = 5;
+    optional uint64 min_amount = 4;
+    optional uint64 max_amount = 5;
+    optional MeltMethodOptions options = 6;
 }
 
 message UpdateQuoteTtlRequest {

+ 28 - 13
crates/cdk-mint-rpc/src/proto/server.rs

@@ -465,22 +465,29 @@ impl CdkMint for MintRPCServer {
 
         let mut methods = nut04_settings.methods.clone();
 
+        // Create options from the request
+        let options = if let Some(options) = request_inner.options {
+            Some(cdk::nuts::nut04::MintMethodOptions::Bolt11 {
+                description: options.description,
+            })
+        } else if let Some(current_settings) = current_nut04_settings.as_ref() {
+            current_settings.options.clone()
+        } else {
+            None
+        };
+
         let updated_method_settings = MintMethodSettings {
             method: payment_method,
             unit,
             min_amount: request_inner
-                .min
+                .min_amount
                 .map(Amount::from)
                 .or_else(|| current_nut04_settings.as_ref().and_then(|s| s.min_amount)),
             max_amount: request_inner
-                .max
+                .max_amount
                 .map(Amount::from)
                 .or_else(|| current_nut04_settings.as_ref().and_then(|s| s.max_amount)),
-            description: request_inner.description.unwrap_or(
-                current_nut04_settings
-                    .map(|c| c.description)
-                    .unwrap_or_default(),
-            ),
+            options,
         };
 
         methods.push(updated_method_settings);
@@ -529,21 +536,29 @@ impl CdkMint for MintRPCServer {
 
         let mut methods = nut05_settings.methods;
 
+        // Create options from the request
+        let options = if let Some(options) = request_inner.options {
+            Some(cdk::nuts::nut05::MeltMethodOptions::Bolt11 {
+                amountless: options.amountless,
+            })
+        } else if let Some(current_settings) = current_nut05_settings.as_ref() {
+            current_settings.options.clone()
+        } else {
+            None
+        };
+
         let updated_method_settings = MeltMethodSettings {
             method: payment_method,
             unit,
             min_amount: request_inner
-                .min
+                .min_amount
                 .map(Amount::from)
                 .or_else(|| current_nut05_settings.as_ref().and_then(|s| s.min_amount)),
             max_amount: request_inner
-                .max
+                .max_amount
                 .map(Amount::from)
                 .or_else(|| current_nut05_settings.as_ref().and_then(|s| s.max_amount)),
-            amountless: current_nut05_settings
-                .as_ref()
-                .map(|s| s.amountless)
-                .unwrap_or_default(),
+            options,
         };
 
         methods.push(updated_method_settings);

+ 7 - 7
crates/cdk-payment-processor/src/proto/mod.rs

@@ -83,16 +83,16 @@ impl From<cdk_common::payment::PaymentQuoteResponse> for PaymentQuoteResponse {
     }
 }
 
-impl From<cdk_common::nut05::MeltOptions> for MeltOptions {
-    fn from(value: cdk_common::nut05::MeltOptions) -> Self {
+impl From<cdk_common::nut23::MeltOptions> for MeltOptions {
+    fn from(value: cdk_common::nut23::MeltOptions) -> Self {
         Self {
             options: Some(value.into()),
         }
     }
 }
 
-impl From<cdk_common::nut05::MeltOptions> for Options {
-    fn from(value: cdk_common::nut05::MeltOptions) -> Self {
+impl From<cdk_common::nut23::MeltOptions> for Options {
+    fn from(value: cdk_common::nut23::MeltOptions) -> Self {
         match value {
             cdk_common::MeltOptions::Mpp { mpp } => Self::Mpp(Mpp {
                 amount: mpp.amount.into(),
@@ -104,7 +104,7 @@ impl From<cdk_common::nut05::MeltOptions> for Options {
     }
 }
 
-impl From<MeltOptions> for cdk_common::nut05::MeltOptions {
+impl From<MeltOptions> for cdk_common::nut23::MeltOptions {
     fn from(value: MeltOptions) -> Self {
         let options = value.options.expect("option defined");
         match options {
@@ -152,8 +152,8 @@ impl From<cdk_common::nut05::QuoteState> for QuoteState {
     }
 }
 
-impl From<cdk_common::nut04::QuoteState> for QuoteState {
-    fn from(value: cdk_common::nut04::QuoteState) -> Self {
+impl From<cdk_common::nut23::QuoteState> for QuoteState {
+    fn from(value: cdk_common::nut23::QuoteState) -> Self {
         match value {
             cdk_common::MintQuoteState::Unpaid => Self::Unpaid,
             cdk_common::MintQuoteState::Paid => Self::Paid,

+ 4 - 4
crates/cdk-redb/src/mint/mod.rs

@@ -18,8 +18,8 @@ use cdk_common::nut00::ProofsMethods;
 use cdk_common::state::check_state_transition;
 use cdk_common::util::unix_time;
 use cdk_common::{
-    BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintInfo, MintQuoteState,
-    Proof, Proofs, PublicKey, State,
+    BlindSignature, CurrencyUnit, Id, MeltQuoteState, MeltRequest, MintInfo, MintQuoteState, Proof,
+    Proofs, PublicKey, State,
 };
 use migrations::{migrate_01_to_02, migrate_04_to_05};
 use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
@@ -543,7 +543,7 @@ impl MintQuotesDatabase for MintRedbDatabase {
     /// Add melt request
     async fn add_melt_request(
         &self,
-        melt_request: MeltBolt11Request<Uuid>,
+        melt_request: MeltRequest<Uuid>,
         ln_key: PaymentProcessorKey,
     ) -> Result<(), Self::Err> {
         let write_txn = self.db.begin_write().map_err(Error::from)?;
@@ -565,7 +565,7 @@ impl MintQuotesDatabase for MintRedbDatabase {
     async fn get_melt_request(
         &self,
         quote_id: &Uuid,
-    ) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err> {
+    ) -> Result<Option<(MeltRequest<Uuid>, PaymentProcessorKey)>, Self::Err> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let table = read_txn.open_table(MELT_REQUESTS).map_err(Error::from)?;
 

+ 3 - 0
crates/cdk-sqlite/src/mint/error.rs

@@ -26,6 +26,9 @@ pub enum Error {
     /// NUT07 Error
     #[error(transparent)]
     CDKNUT07(#[from] cdk_common::nuts::nut07::Error),
+    /// NUT23 Error
+    #[error(transparent)]
+    CDKNUT23(#[from] cdk_common::nuts::nut23::Error),
     /// Secret Error
     #[error(transparent)]
     CDKSECRET(#[from] cdk_common::secret::Error),

+ 2 - 2
crates/cdk-sqlite/src/mint/memory.rs

@@ -6,7 +6,7 @@ use cdk_common::database::{
     self, MintDatabase, MintKeysDatabase, MintProofsDatabase, MintQuotesDatabase,
 };
 use cdk_common::mint::{self, MintKeySetInfo, MintQuote};
-use cdk_common::nuts::{CurrencyUnit, Id, MeltBolt11Request, Proofs};
+use cdk_common::nuts::{CurrencyUnit, Id, MeltRequest, Proofs};
 use cdk_common::MintInfo;
 use uuid::Uuid;
 
@@ -30,7 +30,7 @@ pub async fn new_with_state(
     melt_quotes: Vec<mint::MeltQuote>,
     pending_proofs: Proofs,
     spent_proofs: Proofs,
-    melt_request: Vec<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>,
+    melt_request: Vec<(MeltRequest<Uuid>, PaymentProcessorKey)>,
     mint_info: MintInfo,
 ) -> Result<MintSqliteDatabase, database::Error> {
     let db = empty().await?;

+ 6 - 7
crates/cdk-sqlite/src/mint/mod.rs

@@ -18,9 +18,8 @@ use cdk_common::secret::Secret;
 use cdk_common::state::check_state_transition;
 use cdk_common::util::unix_time;
 use cdk_common::{
-    Amount, BlindSignature, BlindSignatureDleq, CurrencyUnit, Id, MeltBolt11Request,
-    MeltQuoteState, MintInfo, MintQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SecretKey,
-    State,
+    Amount, BlindSignature, BlindSignatureDleq, CurrencyUnit, Id, MeltQuoteState, MeltRequest,
+    MintInfo, MintQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SecretKey, State,
 };
 use error::Error;
 use lightning_invoice::Bolt11Invoice;
@@ -946,7 +945,7 @@ WHERE id=?
 
     async fn add_melt_request(
         &self,
-        melt_request: MeltBolt11Request<Uuid>,
+        melt_request: MeltRequest<Uuid>,
         ln_key: PaymentProcessorKey,
     ) -> Result<(), Self::Err> {
         let mut transaction = self.pool.begin().await.map_err(Error::from)?;
@@ -990,7 +989,7 @@ ON CONFLICT(id) DO UPDATE SET
     async fn get_melt_request(
         &self,
         quote_id: &Uuid,
-    ) -> Result<Option<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>, Self::Err> {
+    ) -> Result<Option<(MeltRequest<Uuid>, PaymentProcessorKey)>, Self::Err> {
         let mut transaction = self.pool.begin().await.map_err(Error::from)?;
 
         let rec = sqlx::query(
@@ -1818,14 +1817,14 @@ fn sqlite_row_to_blind_signature(row: SqliteRow) -> Result<BlindSignature, Error
 
 fn sqlite_row_to_melt_request(
     row: SqliteRow,
-) -> Result<(MeltBolt11Request<Uuid>, PaymentProcessorKey), Error> {
+) -> Result<(MeltRequest<Uuid>, PaymentProcessorKey), Error> {
     let quote_id: Hyphenated = row.try_get("id").map_err(Error::from)?;
     let row_inputs: String = row.try_get("inputs").map_err(Error::from)?;
     let row_outputs: Option<String> = row.try_get("outputs").map_err(Error::from)?;
     let row_method: String = row.try_get("method").map_err(Error::from)?;
     let row_unit: String = row.try_get("unit").map_err(Error::from)?;
 
-    let melt_request = MeltBolt11Request::new(
+    let melt_request = MeltRequest::new(
         quote_id.into_uuid(),
         serde_json::from_str(&row_inputs)?,
         row_outputs.and_then(|o| serde_json::from_str(&o).ok()),

+ 3 - 0
crates/cdk-sqlite/src/wallet/error.rs

@@ -32,6 +32,9 @@ pub enum Error {
     /// NUT07 Error
     #[error(transparent)]
     CDKNUT07(#[from] cdk_common::nuts::nut07::Error),
+    /// NUT23 Error
+    #[error(transparent)]
+    CDKNUT23(#[from] cdk_common::nuts::nut23::Error),
     /// Secret Error
     #[error(transparent)]
     CDKSECRET(#[from] cdk_common::secret::Error),

+ 8 - 2
crates/cdk/src/mint/builder.rs

@@ -7,6 +7,8 @@ use anyhow::anyhow;
 use bitcoin::bip32::DerivationPath;
 use cdk_common::database::{self, MintDatabase};
 use cdk_common::error::Error;
+use cdk_common::nut04::MintMethodOptions;
+use cdk_common::nut05::MeltMethodOptions;
 use cdk_common::payment::Bolt11Settings;
 use cdk_common::{nut21, nut22};
 
@@ -195,7 +197,9 @@ impl MintBuilder {
                 unit: unit.clone(),
                 min_amount: Some(limits.mint_min),
                 max_amount: Some(limits.mint_max),
-                description: settings.invoice_description,
+                options: Some(MintMethodOptions::Bolt11 {
+                    description: settings.invoice_description,
+                }),
             };
 
             self.mint_info.nuts.nut04.methods.push(mint_method_settings);
@@ -206,7 +210,9 @@ impl MintBuilder {
                 unit,
                 min_amount: Some(limits.melt_min),
                 max_amount: Some(limits.melt_max),
-                amountless: settings.amountless,
+                options: Some(MeltMethodOptions::Bolt11 {
+                    amountless: settings.amountless,
+                }),
             };
             self.mint_info.nuts.nut05.methods.push(melt_method_settings);
             self.mint_info.nuts.nut05.disabled = false;

+ 3 - 3
crates/cdk/src/mint/issue/auth.rs

@@ -1,7 +1,7 @@
 use tracing::instrument;
 
 use crate::mint::nut22::MintAuthRequest;
-use crate::mint::{AuthToken, MintBolt11Response};
+use crate::mint::{AuthToken, MintResponse};
 use crate::{Amount, Error, Mint};
 
 impl Mint {
@@ -11,7 +11,7 @@ impl Mint {
         &self,
         auth_token: AuthToken,
         mint_auth_request: MintAuthRequest,
-    ) -> Result<MintBolt11Response, Error> {
+    ) -> Result<MintResponse, Error> {
         let cat = if let AuthToken::ClearAuth(cat) = auth_token {
             cat
         } else {
@@ -47,7 +47,7 @@ impl Mint {
             blind_signatures.push(blind_signature);
         }
 
-        Ok(MintBolt11Response {
+        Ok(MintResponse {
             signatures: blind_signatures,
         })
     }

+ 5 - 5
crates/cdk/src/mint/issue/issue_nut04.rs

@@ -3,8 +3,8 @@ use tracing::instrument;
 use uuid::Uuid;
 
 use crate::mint::{
-    CurrencyUnit, MintBolt11Request, MintBolt11Response, MintQuote, MintQuoteBolt11Request,
-    MintQuoteBolt11Response, MintQuoteState, NotificationPayload, PublicKey, Verification,
+    CurrencyUnit, MintQuote, MintQuoteBolt11Request, MintQuoteBolt11Response, MintQuoteState,
+    MintRequest, MintResponse, NotificationPayload, PublicKey, Verification,
 };
 use crate::nuts::PaymentMethod;
 use crate::util::unix_time;
@@ -236,8 +236,8 @@ impl Mint {
     #[instrument(skip_all)]
     pub async fn process_mint_request(
         &self,
-        mint_request: MintBolt11Request<Uuid>,
-    ) -> Result<MintBolt11Response, Error> {
+        mint_request: MintRequest<Uuid>,
+    ) -> Result<MintResponse, Error> {
         let mint_quote = self
             .localstore
             .get_mint_quote(&mint_request.quote)
@@ -332,7 +332,7 @@ impl Mint {
         self.pubsub_manager
             .mint_quote_bolt11_status(mint_quote, MintQuoteState::Issued);
 
-        Ok(MintBolt11Response {
+        Ok(MintResponse {
             signatures: blind_signatures,
         })
     }

+ 12 - 11
crates/cdk/src/mint/melt.rs

@@ -2,14 +2,15 @@ use std::str::FromStr;
 
 use anyhow::bail;
 use cdk_common::nut00::ProofsMethods;
+use cdk_common::nut05::MeltMethodOptions;
 use cdk_common::MeltOptions;
 use lightning_invoice::Bolt11Invoice;
 use tracing::instrument;
 use uuid::Uuid;
 
 use super::{
-    CurrencyUnit, MeltBolt11Request, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
-    Mint, PaymentMethod, PublicKey, State,
+    CurrencyUnit, MeltQuote, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, Mint,
+    PaymentMethod, PublicKey, State,
 };
 use crate::amount::to_unit;
 use crate::cdk_payment::{MakePaymentResponse, MintPayment};
@@ -62,7 +63,10 @@ impl Mint {
                 amount
             }
             Some(MeltOptions::Amountless { amountless: _ }) => {
-                if !settings.amountless {
+                if !matches!(
+                    settings.options,
+                    Some(MeltMethodOptions::Bolt11 { amountless: true })
+                ) {
                     return Err(Error::AmountlessInvoiceNotSupported(unit, method));
                 }
 
@@ -235,7 +239,7 @@ impl Mint {
     pub async fn check_melt_expected_ln_fees(
         &self,
         melt_quote: &MeltQuote,
-        melt_request: &MeltBolt11Request<Uuid>,
+        melt_request: &MeltRequest<Uuid>,
     ) -> Result<Option<Amount>, Error> {
         let invoice = Bolt11Invoice::from_str(&melt_quote.request)?;
 
@@ -291,7 +295,7 @@ impl Mint {
     #[instrument(skip_all)]
     pub async fn verify_melt_request(
         &self,
-        melt_request: &MeltBolt11Request<Uuid>,
+        melt_request: &MeltRequest<Uuid>,
     ) -> Result<MeltQuote, Error> {
         let state = self
             .localstore
@@ -377,10 +381,7 @@ impl Mint {
     /// made The proofs should be returned to an unspent state and the
     /// quote should be unpaid
     #[instrument(skip_all)]
-    pub async fn process_unpaid_melt(
-        &self,
-        melt_request: &MeltBolt11Request<Uuid>,
-    ) -> Result<(), Error> {
+    pub async fn process_unpaid_melt(&self, melt_request: &MeltRequest<Uuid>) -> Result<(), Error> {
         let input_ys = melt_request.inputs().ys()?;
 
         self.localstore
@@ -408,7 +409,7 @@ impl Mint {
     #[instrument(skip_all)]
     pub async fn melt_bolt11(
         &self,
-        melt_request: &MeltBolt11Request<Uuid>,
+        melt_request: &MeltRequest<Uuid>,
     ) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {
         use std::sync::Arc;
         async fn check_payment_state(
@@ -619,7 +620,7 @@ impl Mint {
     #[instrument(skip_all)]
     pub async fn process_melt_request(
         &self,
-        melt_request: &MeltBolt11Request<Uuid>,
+        melt_request: &MeltRequest<Uuid>,
         payment_preimage: Option<String>,
         total_spent: Amount,
     ) -> Result<MeltQuoteBolt11Response<Uuid>, Error> {

+ 2 - 2
crates/cdk/src/mint/mod.rs

@@ -482,7 +482,7 @@ impl Mint {
     pub async fn handle_internal_melt_mint(
         &self,
         melt_quote: &MeltQuote,
-        melt_request: &MeltBolt11Request<Uuid>,
+        melt_request: &MeltRequest<Uuid>,
     ) -> Result<Option<Amount>, Error> {
         let mint_quote = match self
             .localstore
@@ -761,7 +761,7 @@ mod tests {
         seed: &'a [u8],
         mint_info: MintInfo,
         supported_units: HashMap<CurrencyUnit, (u64, u8)>,
-        melt_requests: Vec<(MeltBolt11Request<Uuid>, PaymentProcessorKey)>,
+        melt_requests: Vec<(MeltRequest<Uuid>, PaymentProcessorKey)>,
     }
 
     async fn create_mint(config: MintConfig<'_>) -> Mint {

+ 2 - 5
crates/cdk/src/wallet/auth/auth_connector.rs

@@ -4,7 +4,7 @@ use async_trait::async_trait;
 use cdk_common::{AuthToken, MintInfo};
 
 use super::Error;
-use crate::nuts::{Id, KeySet, KeysetResponse, MintAuthRequest, MintBolt11Response};
+use crate::nuts::{Id, KeySet, KeysetResponse, MintAuthRequest, MintResponse};
 
 /// Interface that connects a wallet to a mint. Typically represents an HttpClient.
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -23,8 +23,5 @@ pub trait AuthMintConnector: Debug {
     /// Get Blind Auth keysets
     async fn get_mint_blind_auth_keysets(&self) -> Result<KeysetResponse, Error>;
     /// Post mint blind auth
-    async fn post_mint_blind_auth(
-        &self,
-        request: MintAuthRequest,
-    ) -> Result<MintBolt11Response, Error>;
+    async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result<MintResponse, Error>;
 }

+ 2 - 2
crates/cdk/src/wallet/melt.rs

@@ -9,7 +9,7 @@ use super::MeltQuote;
 use crate::amount::to_unit;
 use crate::dhke::construct_proofs;
 use crate::nuts::{
-    CurrencyUnit, MeltBolt11Request, MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
+    CurrencyUnit, MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest,
     PreMintSecrets, Proofs, ProofsMethods, State,
 };
 use crate::types::{Melted, ProofInfo};
@@ -152,7 +152,7 @@ impl Wallet {
             proofs_total - quote_info.amount,
         )?;
 
-        let request = MeltBolt11Request::new(
+        let request = MeltRequest::new(
             quote_id.to_string(),
             proofs.clone(),
             Some(premint_secrets.blinded_messages()),

+ 8 - 5
crates/cdk/src/wallet/mint.rs

@@ -1,6 +1,6 @@
 use std::collections::HashMap;
 
-use cdk_common::ensure_cdk;
+use cdk_common::nut04::MintMethodOptions;
 use cdk_common::wallet::{Transaction, TransactionDirection};
 use tracing::instrument;
 
@@ -9,8 +9,8 @@ use crate::amount::SplitTarget;
 use crate::dhke::construct_proofs;
 use crate::nuts::nut00::ProofsMethods;
 use crate::nuts::{
-    nut12, MintBolt11Request, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets,
-    Proofs, SecretKey, SpendingConditions, State,
+    nut12, MintQuoteBolt11Request, MintQuoteBolt11Response, MintRequest, PreMintSecrets, Proofs,
+    SecretKey, SpendingConditions, State,
 };
 use crate::types::ProofInfo;
 use crate::util::unix_time;
@@ -64,7 +64,10 @@ impl Wallet {
                 .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11)
                 .ok_or(Error::UnsupportedUnit)?;
 
-            ensure_cdk!(settings.description, Error::InvoiceDescriptionUnsupported);
+            match settings.options {
+                Some(MintMethodOptions::Bolt11 { description }) if description => (),
+                _ => return Err(Error::InvoiceDescriptionUnsupported),
+            }
         }
 
         let secret_key = SecretKey::generate();
@@ -224,7 +227,7 @@ impl Wallet {
             )?,
         };
 
-        let mut request = MintBolt11Request {
+        let mut request = MintRequest {
             quote: quote_id.to_string(),
             outputs: premint_secrets.blinded_messages(),
             signature: None,

+ 6 - 12
crates/cdk/src/wallet/mint_connector/http_client.rs

@@ -20,9 +20,9 @@ use crate::mint_url::MintUrl;
 use crate::nuts::nut22::MintAuthRequest;
 use crate::nuts::{
     AuthToken, CheckStateRequest, CheckStateResponse, Id, KeySet, KeysResponse, KeysetResponse,
-    MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request,
-    MintBolt11Response, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest,
-    RestoreResponse, SwapRequest, SwapResponse,
+    MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
+    MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
+    SwapRequest, SwapResponse,
 };
 #[cfg(feature = "auth")]
 use crate::wallet::auth::{AuthMintConnector, AuthWallet};
@@ -263,10 +263,7 @@ impl MintConnector for HttpClient {
 
     /// Mint Tokens [NUT-04]
     #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
-    async fn post_mint(
-        &self,
-        request: MintBolt11Request<String>,
-    ) -> Result<MintBolt11Response, Error> {
+    async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error> {
         let url = self.mint_url.join_paths(&["v1", "mint", "bolt11"])?;
         #[cfg(feature = "auth")]
         let auth_token = self
@@ -322,7 +319,7 @@ impl MintConnector for HttpClient {
     #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
     async fn post_melt(
         &self,
-        request: MeltBolt11Request<String>,
+        request: MeltRequest<String>,
     ) -> Result<MeltQuoteBolt11Response<String>, Error> {
         let url = self.mint_url.join_paths(&["v1", "melt", "bolt11"])?;
         #[cfg(feature = "auth")]
@@ -469,10 +466,7 @@ impl AuthMintConnector for AuthHttpClient {
 
     /// Mint Tokens [NUT-22]
     #[instrument(skip(self, request), fields(mint_url = %self.mint_url))]
-    async fn post_mint_blind_auth(
-        &self,
-        request: MintAuthRequest,
-    ) -> Result<MintBolt11Response, Error> {
+    async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result<MintResponse, Error> {
         let url = self.mint_url.join_paths(&["v1", "auth", "blind", "mint"])?;
         self.core
             .http_post(url, Some(self.cat.read().await.clone()), &request)

+ 5 - 8
crates/cdk/src/wallet/mint_connector/mod.rs

@@ -6,9 +6,9 @@ use async_trait::async_trait;
 
 use super::Error;
 use crate::nuts::{
-    CheckStateRequest, CheckStateResponse, Id, KeySet, KeysetResponse, MeltBolt11Request,
-    MeltQuoteBolt11Request, MeltQuoteBolt11Response, MintBolt11Request, MintBolt11Response,
-    MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, RestoreRequest, RestoreResponse,
+    CheckStateRequest, CheckStateResponse, Id, KeySet, KeysetResponse, MeltQuoteBolt11Request,
+    MeltQuoteBolt11Response, MeltRequest, MintInfo, MintQuoteBolt11Request,
+    MintQuoteBolt11Response, MintRequest, MintResponse, RestoreRequest, RestoreResponse,
     SwapRequest, SwapResponse,
 };
 #[cfg(feature = "auth")]
@@ -41,10 +41,7 @@ pub trait MintConnector: Debug {
         quote_id: &str,
     ) -> Result<MintQuoteBolt11Response<String>, Error>;
     /// Mint Tokens [NUT-04]
-    async fn post_mint(
-        &self,
-        request: MintBolt11Request<String>,
-    ) -> Result<MintBolt11Response, Error>;
+    async fn post_mint(&self, request: MintRequest<String>) -> Result<MintResponse, Error>;
     /// Melt Quote [NUT-05]
     async fn post_melt_quote(
         &self,
@@ -59,7 +56,7 @@ pub trait MintConnector: Debug {
     /// [Nut-08] Lightning fee return if outputs defined
     async fn post_melt(
         &self,
-        request: MeltBolt11Request<String>,
+        request: MeltRequest<String>,
     ) -> Result<MeltQuoteBolt11Response<String>, Error>;
     /// Split Token [NUT-06]
     async fn post_swap(&self, request: SwapRequest) -> Result<SwapResponse, Error>;

+ 2 - 2
crates/cdk/src/wallet/subscription/http.rs

@@ -7,7 +7,7 @@ use tokio::time;
 
 use super::WsSubscriptionBody;
 use crate::nuts::nut17::Kind;
-use crate::nuts::{nut01, nut04, nut05, nut07, CheckStateRequest, NotificationPayload};
+use crate::nuts::{nut01, nut05, nut07, nut23, CheckStateRequest, NotificationPayload};
 use crate::pub_sub::SubId;
 use crate::wallet::MintConnector;
 use crate::Wallet;
@@ -21,7 +21,7 @@ enum UrlType {
 
 #[derive(Debug, Eq, PartialEq)]
 enum AnyState {
-    MintQuoteState(nut04::QuoteState),
+    MintQuoteState(nut23::QuoteState),
     MeltQuoteState(nut05::QuoteState),
     PublicKey(nut07::State),
     Empty,