diff --git a/crates/cashu/src/nuts/mod.rs b/crates/cashu/src/nuts/mod.rs index 5d76f8b1..d9e31218 100644 --- a/crates/cashu/src/nuts/mod.rs +++ b/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, +}; diff --git a/crates/cashu/src/nuts/nut04.rs b/crates/cashu/src/nuts/nut04.rs index d3bf4945..68fbcc97 100644 --- a/crates/cashu/src/nuts/nut04.rs +++ b/crates/cashu/src/nuts/nut04.rs @@ -3,16 +3,17 @@ //! 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, - /// NUT-19 Pubkey - #[serde(skip_serializing_if = "Option::is_none")] - pub pubkey: Option, -} - -/// 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 { - 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 { - /// 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, - /// Unit - // REVIEW: This is now required in the spec, we should remove the option once all mints update - pub unit: Option, - /// Quote State - pub state: MintQuoteState, - /// Unix timestamp until the quote is valid - pub expiry: Option, - /// NUT-19 Pubkey - #[serde(skip_serializing_if = "Option::is_none")] - pub pubkey: Option, -} - -impl MintQuoteBolt11Response { - /// Convert the MintQuote with a quote type Q to a String - pub fn to_string_id(&self) -> MintQuoteBolt11Response { - 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> for MintQuoteBolt11Response { - fn from(value: MintQuoteBolt11Response) -> 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 { +pub struct MintRequest { /// Quote id #[cfg_attr(feature = "swagger", schema(max_length = 1_000))] pub quote: Q, @@ -156,10 +44,10 @@ pub struct MintBolt11Request { } #[cfg(feature = "mint")] -impl TryFrom> for MintBolt11Request { +impl TryFrom> for MintRequest { type Error = uuid::Error; - fn try_from(value: MintBolt11Request) -> Result { + fn try_from(value: MintRequest) -> Result { Ok(Self { quote: Uuid::from_str(&value.quote)?, outputs: value.outputs, @@ -168,7 +56,7 @@ impl TryFrom> for MintBolt11Request { } } -impl MintBolt11Request { +impl MintRequest { /// Total [`Amount`] of outputs pub fn total_amount(&self) -> Result { Amount::try_sum( @@ -183,13 +71,13 @@ impl MintBolt11Request { /// 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, } /// 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, /// Max Amount - #[serde(skip_serializing_if = "Option::is_none")] pub max_amount: Option, - /// Quote Description - #[serde(default)] - pub description: bool, + /// Options + pub options: Option, +} + +impl Serialize for MintMethodSettings { + fn serialize(&self, serializer: S) -> Result + 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(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut method: Option = None; + let mut unit: Option = None; + let mut min_amount: Option = None; + let mut max_amount: Option = None; + let mut description: Option = None; + + while let Some(key) = map.next_key::()? { + 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 = 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(deserializer: D) -> Result + 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"), + } + } +} diff --git a/crates/cashu/src/nuts/nut05.rs b/crates/cashu/src/nuts/nut05.rs index 5a3ab1b5..cd965274 100644 --- a/crates/cashu/src/nuts/nut05.rs +++ b/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, -} - -/// 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(amount: A) -> Self - where - A: Into, - { - Self::Mpp { - mpp: Mpp { - amount: amount.into(), - }, - } - } - - /// Create new [`MeltOptions::Amountless`] - pub fn new_amountless(amount_msat: A) -> Self - where - A: Into, - { - 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 { - 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 { - /// 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, - /// 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, - /// Change - #[serde(skip_serializing_if = "Option::is_none")] - pub change: Option>, - /// 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, - /// 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, -} - -impl MeltQuoteBolt11Response { - /// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a - /// `MeltQuoteBolt11Response` with `String` - pub fn to_string_id(self) -> MeltQuoteBolt11Response { - 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> for MeltQuoteBolt11Response { - fn from(value: MeltQuoteBolt11Response) -> 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 { - fn deserialize(deserializer: D) -> Result - 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 = value.get("paid").and_then(|p| p.as_bool()); - - let state: Option = 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 = value - .get("payment_preimage") - .and_then(|p| serde_json::from_value(p.clone()).ok()); - - let change: Option> = value - .get("change") - .and_then(|b| serde_json::from_value(b.clone()).ok()); - - let request: Option = value - .get("request") - .and_then(|r| serde_json::from_value(r.clone()).ok()); - - let unit: Option = 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 { +pub struct MeltRequest { /// Quote ID quote: Q, /// Proofs @@ -366,10 +91,10 @@ pub struct MeltBolt11Request { } #[cfg(feature = "mint")] -impl TryFrom> for MeltBolt11Request { +impl TryFrom> for MeltRequest { type Error = uuid::Error; - fn try_from(value: MeltBolt11Request) -> Result { + fn try_from(value: MeltRequest) -> Result { Ok(Self { quote: Uuid::from_str(&value.quote)?, inputs: value.inputs, @@ -379,7 +104,7 @@ impl TryFrom> for MeltBolt11Request { } // Basic implementation without trait bounds -impl MeltBolt11Request { +impl MeltRequest { /// Get inputs (proofs) pub fn inputs(&self) -> &Proofs { &self.inputs @@ -391,8 +116,8 @@ impl MeltBolt11Request { } } -impl MeltBolt11Request { - /// Create new [`MeltBolt11Request`] +impl MeltRequest { + /// Create new [`MeltRequest`] pub fn new(quote: Q, inputs: Proofs, outputs: Option>) -> Self { Self { quote, @@ -414,7 +139,7 @@ impl MeltBolt11Request { } /// 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, /// Max Amount - #[serde(skip_serializing_if = "Option::is_none")] pub max_amount: Option, - /// Amountless - #[serde(default)] - pub amountless: bool, + /// Options + pub options: Option, +} + +impl Serialize for MeltMethodSettings { + fn serialize(&self, serializer: S) -> Result + 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(self, mut map: M) -> Result + where + M: MapAccess<'de>, + { + let mut method: Option = None; + let mut unit: Option = None; + let mut min_amount: Option = None; + let mut max_amount: Option = None; + let mut amountless: Option = None; + + while let Some(key) = map.next_key::()? { + 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 = 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(deserializer: D) -> Result + 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"), + } + } +} diff --git a/crates/cashu/src/nuts/nut06.rs b/crates/cashu/src/nuts/nut06.rs index c13a9782..9dfbe563 100644 --- a/crates/cashu/src/nuts/nut06.rs +++ b/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); } } diff --git a/crates/cashu/src/nuts/nut08.rs b/crates/cashu/src/nuts/nut08.rs index 7a5325cf..12869174 100644 --- a/crates/cashu/src/nuts/nut08.rs +++ b/crates/cashu/src/nuts/nut08.rs @@ -2,10 +2,11 @@ //! //! -use super::nut05::{MeltBolt11Request, MeltQuoteBolt11Response}; +use super::nut05::MeltRequest; +use super::nut23::MeltQuoteBolt11Response; use crate::Amount; -impl MeltBolt11Request { +impl MeltRequest { /// Total output [`Amount`] pub fn output_amount(&self) -> Option { self.outputs() diff --git a/crates/cashu/src/nuts/nut20.rs b/crates/cashu/src/nuts/nut20.rs index d3890e17..4546dc73 100644 --- a/crates/cashu/src/nuts/nut20.rs +++ b/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 MintBolt11Request +impl MintRequest 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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()); diff --git a/crates/cashu/src/nuts/nut23.rs b/crates/cashu/src/nuts/nut23.rs new file mode 100644 index 00000000..e80f480d --- /dev/null +++ b/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, + /// NUT-19 Pubkey + #[serde(skip_serializing_if = "Option::is_none")] + pub pubkey: Option, +} + +/// 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 { + 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 { + /// 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, + /// Unit + // REVIEW: This is now required in the spec, we should remove the option once all mints update + pub unit: Option, + /// Quote State + pub state: QuoteState, + /// Unix timestamp until the quote is valid + pub expiry: Option, + /// NUT-19 Pubkey + #[serde(skip_serializing_if = "Option::is_none")] + pub pubkey: Option, +} +impl MintQuoteBolt11Response { + /// Convert the MintQuote with a quote type Q to a String + pub fn to_string_id(&self) -> MintQuoteBolt11Response { + 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> for MintQuoteBolt11Response { + fn from(value: MintQuoteBolt11Response) -> 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, +} + +/// 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(amount: A) -> Self + where + A: Into, + { + Self::Mpp { + mpp: Mpp { + amount: amount.into(), + }, + } + } + + /// Create new [`MeltOptions::Amountless`] + pub fn new_amountless(amount_msat: A) -> Self + where + A: Into, + { + 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 { + 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 { + /// 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, + /// 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, + /// Change + #[serde(skip_serializing_if = "Option::is_none")] + pub change: Option>, + /// 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, + /// 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, +} + +impl MeltQuoteBolt11Response { + /// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a + /// `MeltQuoteBolt11Response` with `String` + pub fn to_string_id(self) -> MeltQuoteBolt11Response { + 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> for MeltQuoteBolt11Response { + fn from(value: MeltQuoteBolt11Response) -> 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 { + fn deserialize(deserializer: D) -> Result + 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 = value.get("paid").and_then(|p| p.as_bool()); + + let state: Option = 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 = value + .get("payment_preimage") + .and_then(|p| serde_json::from_value(p.clone()).ok()); + + let change: Option> = value + .get("change") + .and_then(|b| serde_json::from_value(b.clone()).ok()); + + let request: Option = value + .get("request") + .and_then(|r| serde_json::from_value(r.clone()).ok()); + + let unit: Option = 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, + }) + } +} diff --git a/crates/cdk-axum/src/auth.rs b/crates/cdk-axum/src/auth.rs index b4185ef0..d73337a1 100644 --- a/crates/cdk-axum/src/auth.rs +++ b/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, Json(payload): Json, -) -> Result, Response> { +) -> Result, Response> { let auth_token = match auth { AuthHeader::Clear(cat) => { if cat.is_empty() { diff --git a/crates/cdk-axum/src/lib.rs b/crates/cdk-axum/src/lib.rs index f7e2d432..ec108e36 100644 --- a/crates/cdk-axum/src/lib.rs +++ b/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, + MeltRequest, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltQuoteState, MeltMethodSettings, - MintBolt11Request, - MintBolt11Response, + MintRequest, + MintResponse, MintInfo, MintQuoteBolt11Request, MintQuoteBolt11Response, diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index 8fe8cc7f..681e08c6 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/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, - MintBolt11Response -); +post_cache_wrapper!(post_mint_bolt11, MintRequest, MintResponse); post_cache_wrapper!( post_melt_bolt11, - MeltBolt11Request, + MeltRequest, MeltQuoteBolt11Response ); @@ -246,9 +242,9 @@ pub(crate) async fn ws_handler( post, context_path = "/v1", path = "/mint/bolt11", - request_body(content = MintBolt11Request, description = "Request params", content_type = "application/json"), + request_body(content = MintRequest, 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, - Json(payload): Json>, -) -> Result, Response> { + Json(payload): Json>, +) -> Result, 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, description = "Melt params", content_type = "application/json"), + request_body(content = MeltRequest, description = "Melt params", content_type = "application/json"), responses( (status = 200, description = "Successful response", body = MeltQuoteBolt11Response, 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, - Json(payload): Json>, + Json(payload): Json>, ) -> Result>, Response> { #[cfg(feature = "auth")] { diff --git a/crates/cdk-common/src/database/mint/mod.rs b/crates/cdk-common/src/database/mint/mod.rs index a11e54f9..c2d9b206 100644 --- a/crates/cdk-common/src/database/mint/mod.rs +++ b/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, + melt_request: MeltRequest, ln_key: PaymentProcessorKey, ) -> Result<(), Self::Err>; /// Get melt request async fn get_melt_request( &self, quote_id: &Uuid, - ) -> Result, PaymentProcessorKey)>, Self::Err>; + ) -> Result, PaymentProcessorKey)>, Self::Err>; } /// Mint Proof Database trait diff --git a/crates/cdk-common/src/error.rs b/crates/cdk-common/src/error.rs index 86344de1..aea4ec3f 100644 --- a/crates/cdk-common/src/error.rs +++ b/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), diff --git a/crates/cdk-common/src/payment.rs b/crates/cdk-common/src/payment.rs index 357eeecc..33a2a904 100644 --- a/crates/cdk-common/src/payment.rs +++ b/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), diff --git a/crates/cdk-integration-tests/src/init_pure_tests.rs b/crates/cdk-integration-tests/src/init_pure_tests.rs index 67aa6e02..f9ed35a7 100644 --- a/crates/cdk-integration-tests/src/init_pure_tests.rs +++ b/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, - ) -> Result { + async fn post_mint(&self, request: MintRequest) -> Result { 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, + request: MeltRequest, ) -> Result, Error> { let request_uuid = request.try_into().unwrap(); self.mint.melt_bolt11(&request_uuid).await.map(Into::into) diff --git a/crates/cdk-integration-tests/tests/fake_auth.rs b/crates/cdk-integration-tests/tests/fake_auth.rs index 80bb2e18..d9c97514 100644 --- a/crates/cdk-integration-tests/tests/fake_auth.rs +++ b/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, diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index b19981c3..e2de57c4 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/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; diff --git a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs b/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs index 7097113b..56253f1f 100644 --- a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs +++ b/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()), diff --git a/crates/cdk-integration-tests/tests/integration_tests_pure.rs b/crates/cdk-integration-tests/tests/integration_tests_pure.rs index 17f58907..ca10f257 100644 --- a/crates/cdk-integration-tests/tests/integration_tests_pure.rs +++ b/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(); diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index ec650645..e50aef78 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/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, diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04.rs index c8362cbb..42f9eae7 100644 --- a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut04.rs +++ b/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, 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?; diff --git a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut05.rs b/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut05.rs index df7f98a7..0d350b24 100644 --- a/crates/cdk-mint-rpc/src/mint_rpc_cli/subcommands/update_nut05.rs +++ b/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, + /// Whether amountless bolt11 invoices are allowed + #[arg(long)] + amountless: Option, } /// Executes the update_nut05 command against the mint server @@ -43,13 +46,19 @@ pub async fn update_nut05( client: &mut CdkMintClient, 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?; diff --git a/crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto b/crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto index 37bee454..21b1e3f5 100644 --- a/crates/cdk-mint-rpc/src/proto/cdk-mint-rpc.proto +++ b/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 { diff --git a/crates/cdk-mint-rpc/src/proto/server.rs b/crates/cdk-mint-rpc/src/proto/server.rs index 6f9099a8..1f874660 100644 --- a/crates/cdk-mint-rpc/src/proto/server.rs +++ b/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); diff --git a/crates/cdk-payment-processor/src/proto/mod.rs b/crates/cdk-payment-processor/src/proto/mod.rs index 88e9aeba..df0b47e8 100644 --- a/crates/cdk-payment-processor/src/proto/mod.rs +++ b/crates/cdk-payment-processor/src/proto/mod.rs @@ -83,16 +83,16 @@ impl From for PaymentQuoteResponse { } } -impl From for MeltOptions { - fn from(value: cdk_common::nut05::MeltOptions) -> Self { +impl From for MeltOptions { + fn from(value: cdk_common::nut23::MeltOptions) -> Self { Self { options: Some(value.into()), } } } -impl From for Options { - fn from(value: cdk_common::nut05::MeltOptions) -> Self { +impl From 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 for Options { } } -impl From for cdk_common::nut05::MeltOptions { +impl From 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 for QuoteState { } } -impl From for QuoteState { - fn from(value: cdk_common::nut04::QuoteState) -> Self { +impl From for QuoteState { + fn from(value: cdk_common::nut23::QuoteState) -> Self { match value { cdk_common::MintQuoteState::Unpaid => Self::Unpaid, cdk_common::MintQuoteState::Paid => Self::Paid, diff --git a/crates/cdk-redb/src/mint/mod.rs b/crates/cdk-redb/src/mint/mod.rs index c537affe..0c7b266e 100644 --- a/crates/cdk-redb/src/mint/mod.rs +++ b/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, + melt_request: MeltRequest, 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, PaymentProcessorKey)>, Self::Err> { + ) -> Result, 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)?; diff --git a/crates/cdk-sqlite/src/mint/error.rs b/crates/cdk-sqlite/src/mint/error.rs index 21d1bc65..a4910718 100644 --- a/crates/cdk-sqlite/src/mint/error.rs +++ b/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), diff --git a/crates/cdk-sqlite/src/mint/memory.rs b/crates/cdk-sqlite/src/mint/memory.rs index 19e7e10d..1b40e152 100644 --- a/crates/cdk-sqlite/src/mint/memory.rs +++ b/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, pending_proofs: Proofs, spent_proofs: Proofs, - melt_request: Vec<(MeltBolt11Request, PaymentProcessorKey)>, + melt_request: Vec<(MeltRequest, PaymentProcessorKey)>, mint_info: MintInfo, ) -> Result { let db = empty().await?; diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index 6b106b8c..93298bf9 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/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, + melt_request: MeltRequest, 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, PaymentProcessorKey)>, Self::Err> { + ) -> Result, 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 Result<(MeltBolt11Request, PaymentProcessorKey), Error> { +) -> Result<(MeltRequest, 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 = 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()), diff --git a/crates/cdk-sqlite/src/wallet/error.rs b/crates/cdk-sqlite/src/wallet/error.rs index 3e5aeeb8..f09c8034 100644 --- a/crates/cdk-sqlite/src/wallet/error.rs +++ b/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), diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index 5d43bbf9..98520b87 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/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; diff --git a/crates/cdk/src/mint/issue/auth.rs b/crates/cdk/src/mint/issue/auth.rs index ccdc1286..9f410f29 100644 --- a/crates/cdk/src/mint/issue/auth.rs +++ b/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 { + ) -> Result { 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, }) } diff --git a/crates/cdk/src/mint/issue/issue_nut04.rs b/crates/cdk/src/mint/issue/issue_nut04.rs index 46562e7b..08a0e789 100644 --- a/crates/cdk/src/mint/issue/issue_nut04.rs +++ b/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, - ) -> Result { + mint_request: MintRequest, + ) -> Result { 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, }) } diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index 381689c8..8f454ce1 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/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, + melt_request: &MeltRequest, ) -> Result, 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, + melt_request: &MeltRequest, ) -> Result { 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, - ) -> Result<(), Error> { + pub async fn process_unpaid_melt(&self, melt_request: &MeltRequest) -> 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, + melt_request: &MeltRequest, ) -> Result, 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, + melt_request: &MeltRequest, payment_preimage: Option, total_spent: Amount, ) -> Result, Error> { diff --git a/crates/cdk/src/mint/mod.rs b/crates/cdk/src/mint/mod.rs index 076b9f9c..c554bf23 100644 --- a/crates/cdk/src/mint/mod.rs +++ b/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, + melt_request: &MeltRequest, ) -> Result, Error> { let mint_quote = match self .localstore @@ -761,7 +761,7 @@ mod tests { seed: &'a [u8], mint_info: MintInfo, supported_units: HashMap, - melt_requests: Vec<(MeltBolt11Request, PaymentProcessorKey)>, + melt_requests: Vec<(MeltRequest, PaymentProcessorKey)>, } async fn create_mint(config: MintConfig<'_>) -> Mint { diff --git a/crates/cdk/src/wallet/auth/auth_connector.rs b/crates/cdk/src/wallet/auth/auth_connector.rs index 40919d03..87ca5023 100644 --- a/crates/cdk/src/wallet/auth/auth_connector.rs +++ b/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; /// Post mint blind auth - async fn post_mint_blind_auth( - &self, - request: MintAuthRequest, - ) -> Result; + async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result; } diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index b7fefd85..cbed2ba6 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/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()), diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 99199774..ac71376e 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/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, diff --git a/crates/cdk/src/wallet/mint_connector/http_client.rs b/crates/cdk/src/wallet/mint_connector/http_client.rs index e9baf4a2..1cf1595f 100644 --- a/crates/cdk/src/wallet/mint_connector/http_client.rs +++ b/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, - ) -> Result { + async fn post_mint(&self, request: MintRequest) -> Result { 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, + request: MeltRequest, ) -> Result, 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 { + async fn post_mint_blind_auth(&self, request: MintAuthRequest) -> Result { let url = self.mint_url.join_paths(&["v1", "auth", "blind", "mint"])?; self.core .http_post(url, Some(self.cat.read().await.clone()), &request) diff --git a/crates/cdk/src/wallet/mint_connector/mod.rs b/crates/cdk/src/wallet/mint_connector/mod.rs index bbf9a719..712ba8e9 100644 --- a/crates/cdk/src/wallet/mint_connector/mod.rs +++ b/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, Error>; /// Mint Tokens [NUT-04] - async fn post_mint( - &self, - request: MintBolt11Request, - ) -> Result; + async fn post_mint(&self, request: MintRequest) -> Result; /// 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, + request: MeltRequest, ) -> Result, Error>; /// Split Token [NUT-06] async fn post_swap(&self, request: SwapRequest) -> Result; diff --git a/crates/cdk/src/wallet/subscription/http.rs b/crates/cdk/src/wallet/subscription/http.rs index a14304ef..65a28480 100644 --- a/crates/cdk/src/wallet/subscription/http.rs +++ b/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,