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