cashu/nuts/
nut05.rs

1//! NUT-05: Melting Tokens
2//!
3//! <https://github.com/cashubtc/nuts/blob/main/05.md>
4
5use std::fmt;
6use std::str::FromStr;
7
8use serde::de::DeserializeOwned;
9use serde::{Deserialize, Deserializer, Serialize};
10use serde_json::Value;
11use thiserror::Error;
12#[cfg(feature = "mint")]
13use uuid::Uuid;
14
15use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, Proofs};
16use super::nut15::Mpp;
17use crate::nuts::MeltQuoteState;
18use crate::{Amount, Bolt11Invoice};
19
20/// NUT05 Error
21#[derive(Debug, Error)]
22pub enum Error {
23    /// Unknown Quote State
24    #[error("Unknown quote state")]
25    UnknownState,
26    /// Amount overflow
27    #[error("Amount Overflow")]
28    AmountOverflow,
29    /// Invalid Amount
30    #[error("Invalid Request")]
31    InvalidAmountRequest,
32    /// Unsupported unit
33    #[error("Unsupported unit")]
34    UnsupportedUnit,
35}
36
37/// Melt quote request [NUT-05]
38#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
39#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
40pub struct MeltQuoteBolt11Request {
41    /// Bolt11 invoice to be paid
42    #[cfg_attr(feature = "swagger", schema(value_type = String))]
43    pub request: Bolt11Invoice,
44    /// Unit wallet would like to pay with
45    pub unit: CurrencyUnit,
46    /// Payment Options
47    pub options: Option<MeltOptions>,
48}
49
50/// Melt Options
51#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(untagged)]
53#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
54pub enum MeltOptions {
55    /// Mpp Options
56    Mpp {
57        /// MPP
58        mpp: Mpp,
59    },
60}
61
62impl MeltOptions {
63    /// Create new [`Options::Mpp`]
64    pub fn new_mpp<A>(amount: A) -> Self
65    where
66        A: Into<Amount>,
67    {
68        Self::Mpp {
69            mpp: Mpp {
70                amount: amount.into(),
71            },
72        }
73    }
74
75    /// Payment amount
76    pub fn amount_msat(&self) -> Amount {
77        match self {
78            Self::Mpp { mpp } => mpp.amount,
79        }
80    }
81}
82
83impl MeltQuoteBolt11Request {
84    /// Amount from [`MeltQuoteBolt11Request`]
85    ///
86    /// Amount can either be defined in the bolt11 invoice,
87    /// in the request for an amountless bolt11 or in MPP option.
88    pub fn amount_msat(&self) -> Result<Amount, Error> {
89        let MeltQuoteBolt11Request {
90            request,
91            unit: _,
92            options,
93            ..
94        } = self;
95
96        match options {
97            None => Ok(request
98                .amount_milli_satoshis()
99                .ok_or(Error::InvalidAmountRequest)?
100                .into()),
101            Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
102        }
103    }
104}
105
106/// Possible states of a quote
107#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
108#[serde(rename_all = "UPPERCASE")]
109#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = MeltQuoteState))]
110pub enum QuoteState {
111    /// Quote has not been paid
112    #[default]
113    Unpaid,
114    /// Quote has been paid
115    Paid,
116    /// Paying quote is in progress
117    Pending,
118    /// Unknown state
119    Unknown,
120    /// Failed
121    Failed,
122}
123
124impl fmt::Display for QuoteState {
125    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
126        match self {
127            Self::Unpaid => write!(f, "UNPAID"),
128            Self::Paid => write!(f, "PAID"),
129            Self::Pending => write!(f, "PENDING"),
130            Self::Unknown => write!(f, "UNKNOWN"),
131            Self::Failed => write!(f, "FAILED"),
132        }
133    }
134}
135
136impl FromStr for QuoteState {
137    type Err = Error;
138
139    fn from_str(state: &str) -> Result<Self, Self::Err> {
140        match state {
141            "PENDING" => Ok(Self::Pending),
142            "PAID" => Ok(Self::Paid),
143            "UNPAID" => Ok(Self::Unpaid),
144            "UNKNOWN" => Ok(Self::Unknown),
145            "FAILED" => Ok(Self::Failed),
146            _ => Err(Error::UnknownState),
147        }
148    }
149}
150
151/// Melt quote response [NUT-05]
152#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
153#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
154#[serde(bound = "Q: Serialize")]
155pub struct MeltQuoteBolt11Response<Q> {
156    /// Quote Id
157    pub quote: Q,
158    /// The amount that needs to be provided
159    pub amount: Amount,
160    /// The fee reserve that is required
161    pub fee_reserve: Amount,
162    /// Whether the request haas be paid
163    // TODO: To be deprecated
164    /// Deprecated
165    pub paid: Option<bool>,
166    /// Quote State
167    pub state: MeltQuoteState,
168    /// Unix timestamp until the quote is valid
169    pub expiry: u64,
170    /// Payment preimage
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub payment_preimage: Option<String>,
173    /// Change
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub change: Option<Vec<BlindSignature>>,
176    /// Payment request to fulfill
177    // REVIEW: This is now required in the spec, we should remove the option once all mints update
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub request: Option<String>,
180    /// Unit
181    // REVIEW: This is now required in the spec, we should remove the option once all mints update
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub unit: Option<CurrencyUnit>,
184}
185
186impl<Q: ToString> MeltQuoteBolt11Response<Q> {
187    /// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a
188    /// `MeltQuoteBolt11Response` with `String`
189    pub fn to_string_id(self) -> MeltQuoteBolt11Response<String> {
190        MeltQuoteBolt11Response {
191            quote: self.quote.to_string(),
192            amount: self.amount,
193            fee_reserve: self.fee_reserve,
194            paid: self.paid,
195            state: self.state,
196            expiry: self.expiry,
197            payment_preimage: self.payment_preimage,
198            change: self.change,
199            request: self.request,
200            unit: self.unit,
201        }
202    }
203}
204
205#[cfg(feature = "mint")]
206impl From<MeltQuoteBolt11Response<Uuid>> for MeltQuoteBolt11Response<String> {
207    fn from(value: MeltQuoteBolt11Response<Uuid>) -> Self {
208        Self {
209            quote: value.quote.to_string(),
210            amount: value.amount,
211            fee_reserve: value.fee_reserve,
212            paid: value.paid,
213            state: value.state,
214            expiry: value.expiry,
215            payment_preimage: value.payment_preimage,
216            change: value.change,
217            request: value.request,
218            unit: value.unit,
219        }
220    }
221}
222
223// A custom deserializer is needed until all mints
224// update some will return without the required state.
225impl<'de, Q: DeserializeOwned> Deserialize<'de> for MeltQuoteBolt11Response<Q> {
226    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
227    where
228        D: Deserializer<'de>,
229    {
230        let value = Value::deserialize(deserializer)?;
231
232        let quote: Q = serde_json::from_value(
233            value
234                .get("quote")
235                .ok_or(serde::de::Error::missing_field("quote"))?
236                .clone(),
237        )
238        .map_err(|_| serde::de::Error::custom("Invalid quote if string"))?;
239
240        let amount = value
241            .get("amount")
242            .ok_or(serde::de::Error::missing_field("amount"))?
243            .as_u64()
244            .ok_or(serde::de::Error::missing_field("amount"))?;
245        let amount = Amount::from(amount);
246
247        let fee_reserve = value
248            .get("fee_reserve")
249            .ok_or(serde::de::Error::missing_field("fee_reserve"))?
250            .as_u64()
251            .ok_or(serde::de::Error::missing_field("fee_reserve"))?;
252
253        let fee_reserve = Amount::from(fee_reserve);
254
255        let paid: Option<bool> = value.get("paid").and_then(|p| p.as_bool());
256
257        let state: Option<String> = value
258            .get("state")
259            .and_then(|s| serde_json::from_value(s.clone()).ok());
260
261        let (state, paid) = match (state, paid) {
262            (None, None) => return Err(serde::de::Error::custom("State or paid must be defined")),
263            (Some(state), _) => {
264                let state: QuoteState = QuoteState::from_str(&state)
265                    .map_err(|_| serde::de::Error::custom("Unknown state"))?;
266                let paid = state == QuoteState::Paid;
267
268                (state, paid)
269            }
270            (None, Some(paid)) => {
271                let state = if paid {
272                    QuoteState::Paid
273                } else {
274                    QuoteState::Unpaid
275                };
276                (state, paid)
277            }
278        };
279
280        let expiry = value
281            .get("expiry")
282            .ok_or(serde::de::Error::missing_field("expiry"))?
283            .as_u64()
284            .ok_or(serde::de::Error::missing_field("expiry"))?;
285
286        let payment_preimage: Option<String> = value
287            .get("payment_preimage")
288            .and_then(|p| serde_json::from_value(p.clone()).ok());
289
290        let change: Option<Vec<BlindSignature>> = value
291            .get("change")
292            .and_then(|b| serde_json::from_value(b.clone()).ok());
293
294        let request: Option<String> = value
295            .get("request")
296            .and_then(|r| serde_json::from_value(r.clone()).ok());
297
298        let unit: Option<CurrencyUnit> = value
299            .get("unit")
300            .and_then(|u| serde_json::from_value(u.clone()).ok());
301
302        Ok(Self {
303            quote,
304            amount,
305            fee_reserve,
306            paid: Some(paid),
307            state,
308            expiry,
309            payment_preimage,
310            change,
311            request,
312            unit,
313        })
314    }
315}
316
317/// Melt Bolt11 Request [NUT-05]
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
319#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
320#[serde(bound = "Q: Serialize + DeserializeOwned")]
321pub struct MeltBolt11Request<Q> {
322    /// Quote ID
323    pub quote: Q,
324    /// Proofs
325    #[cfg_attr(feature = "swagger", schema(value_type = Vec<crate::Proof>))]
326    pub inputs: Proofs,
327    /// Blinded Message that can be used to return change [NUT-08]
328    /// Amount field of BlindedMessages `SHOULD` be set to zero
329    pub outputs: Option<Vec<BlindedMessage>>,
330}
331
332#[cfg(feature = "mint")]
333impl TryFrom<MeltBolt11Request<String>> for MeltBolt11Request<Uuid> {
334    type Error = uuid::Error;
335
336    fn try_from(value: MeltBolt11Request<String>) -> Result<Self, Self::Error> {
337        Ok(Self {
338            quote: Uuid::from_str(&value.quote)?,
339            inputs: value.inputs,
340            outputs: value.outputs,
341        })
342    }
343}
344
345impl<Q: Serialize + DeserializeOwned> MeltBolt11Request<Q> {
346    /// Total [`Amount`] of [`Proofs`]
347    pub fn proofs_amount(&self) -> Result<Amount, Error> {
348        Amount::try_sum(self.inputs.iter().map(|proof| proof.amount))
349            .map_err(|_| Error::AmountOverflow)
350    }
351}
352
353/// Melt Method Settings
354#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
355#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
356pub struct MeltMethodSettings {
357    /// Payment Method e.g. bolt11
358    pub method: PaymentMethod,
359    /// Currency Unit e.g. sat
360    pub unit: CurrencyUnit,
361    /// Min Amount
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub min_amount: Option<Amount>,
364    /// Max Amount
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub max_amount: Option<Amount>,
367}
368
369impl Settings {
370    /// Create new [`Settings`]
371    pub fn new(methods: Vec<MeltMethodSettings>, disabled: bool) -> Self {
372        Self { methods, disabled }
373    }
374
375    /// Get [`MeltMethodSettings`] for unit method pair
376    pub fn get_settings(
377        &self,
378        unit: &CurrencyUnit,
379        method: &PaymentMethod,
380    ) -> Option<MeltMethodSettings> {
381        for method_settings in self.methods.iter() {
382            if method_settings.method.eq(method) && method_settings.unit.eq(unit) {
383                return Some(method_settings.clone());
384            }
385        }
386
387        None
388    }
389
390    /// Remove [`MeltMethodSettings`] for unit method pair
391    pub fn remove_settings(
392        &mut self,
393        unit: &CurrencyUnit,
394        method: &PaymentMethod,
395    ) -> Option<MeltMethodSettings> {
396        self.methods
397            .iter()
398            .position(|settings| settings.method.eq(method) && settings.unit.eq(unit))
399            .map(|index| self.methods.remove(index))
400    }
401}
402
403/// Melt Settings
404#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
405#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema), schema(as = nut05::Settings))]
406pub struct Settings {
407    /// Methods to melt
408    pub methods: Vec<MeltMethodSettings>,
409    /// Minting disabled
410    pub disabled: bool,
411}