cashu/nuts/
nut23.rs

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