Skip to main content

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, Serialize};
9use thiserror::Error;
10
11use super::{BlindSignature, CurrencyUnit, MeltQuoteState, Mpp, PublicKey};
12#[cfg(feature = "mint")]
13use crate::quote_id::QuoteId;
14use crate::util::serde_helpers::deserialize_empty_string_as_none;
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(
105        default,
106        skip_serializing_if = "Option::is_none",
107        deserialize_with = "deserialize_empty_string_as_none"
108    )]
109    pub pubkey: Option<PublicKey>,
110}
111impl<Q: ToString> MintQuoteBolt11Response<Q> {
112    /// Convert the MintQuote with a quote type Q to a String
113    pub fn to_string_id(&self) -> MintQuoteBolt11Response<String> {
114        MintQuoteBolt11Response {
115            quote: self.quote.to_string(),
116            request: self.request.clone(),
117            state: self.state,
118            expiry: self.expiry,
119            pubkey: self.pubkey,
120            amount: self.amount,
121            unit: self.unit.clone(),
122        }
123    }
124}
125
126#[cfg(feature = "mint")]
127impl From<MintQuoteBolt11Response<QuoteId>> for MintQuoteBolt11Response<String> {
128    fn from(value: MintQuoteBolt11Response<QuoteId>) -> Self {
129        Self {
130            quote: value.quote.to_string(),
131            request: value.request,
132            state: value.state,
133            expiry: value.expiry,
134            pubkey: value.pubkey,
135            amount: value.amount,
136            unit: value.unit.clone(),
137        }
138    }
139}
140
141/// BOLT11 melt quote request [NUT-23]
142#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
143#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
144pub struct MeltQuoteBolt11Request {
145    /// Bolt11 invoice to be paid
146    #[cfg_attr(feature = "swagger", schema(value_type = String))]
147    pub request: Bolt11Invoice,
148    /// Unit wallet would like to pay with
149    pub unit: CurrencyUnit,
150    /// Payment Options
151    pub options: Option<MeltOptions>,
152}
153
154/// Melt Options
155#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(untagged)]
157#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
158pub enum MeltOptions {
159    /// Mpp Options
160    Mpp {
161        /// MPP
162        mpp: Mpp,
163    },
164    /// Amountless options
165    Amountless {
166        /// Amountless
167        amountless: Amountless,
168    },
169}
170
171impl MeltOptions {
172    /// Create new [`MeltOptions::Mpp`]
173    pub fn new_mpp<A>(amount: A) -> Self
174    where
175        A: Into<Amount>,
176    {
177        Self::Mpp {
178            mpp: Mpp {
179                amount: amount.into(),
180            },
181        }
182    }
183
184    /// Create new [`MeltOptions::Amountless`]
185    pub fn new_amountless<A>(amount_msat: A) -> Self
186    where
187        A: Into<Amount>,
188    {
189        Self::Amountless {
190            amountless: Amountless {
191                amount_msat: amount_msat.into(),
192            },
193        }
194    }
195
196    /// Payment amount
197    pub fn amount_msat(&self) -> Amount {
198        match self {
199            Self::Mpp { mpp } => mpp.amount,
200            Self::Amountless { amountless } => amountless.amount_msat,
201        }
202    }
203}
204
205/// Amountless payment
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
207#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
208pub struct Amountless {
209    /// Amount to pay in msat
210    pub amount_msat: Amount,
211}
212
213impl MeltQuoteBolt11Request {
214    /// Amount from [`MeltQuoteBolt11Request`]
215    ///
216    /// Amount can either be defined in the bolt11 invoice,
217    /// in the request for an amountless bolt11 or in MPP option.
218    pub fn amount_msat(&self) -> Result<Amount, Error> {
219        let MeltQuoteBolt11Request {
220            request, options, ..
221        } = self;
222
223        match options {
224            None => Ok(request
225                .amount_milli_satoshis()
226                .ok_or(Error::InvalidAmountRequest)?
227                .into()),
228            Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
229            Some(MeltOptions::Amountless { amountless }) => {
230                let amount = amountless.amount_msat;
231                if let Some(amount_msat) = request.amount_milli_satoshis() {
232                    if amount != amount_msat.into() {
233                        return Err(Error::InvalidAmountRequest);
234                    }
235                }
236                Ok(amount)
237            }
238        }
239    }
240}
241
242/// Melt quote response [NUT-05]
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
245#[serde(bound = "Q: Serialize + DeserializeOwned")]
246pub struct MeltQuoteBolt11Response<Q> {
247    /// Quote Id
248    pub quote: Q,
249    /// The amount that needs to be provided
250    pub amount: Amount,
251    /// The fee reserve that is required
252    pub fee_reserve: Amount,
253    /// Quote State
254    pub state: MeltQuoteState,
255    /// Unix timestamp until the quote is valid
256    pub expiry: u64,
257    /// Payment preimage
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub payment_preimage: Option<String>,
260    /// Change
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub change: Option<Vec<BlindSignature>>,
263    /// Payment request to fulfill
264    // REVIEW: This is now required in the spec, we should remove the option once all mints update
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub request: Option<String>,
267    /// Unit
268    // REVIEW: This is now required in the spec, we should remove the option once all mints update
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub unit: Option<CurrencyUnit>,
271}
272
273impl<Q: ToString> MeltQuoteBolt11Response<Q> {
274    /// Convert a `MeltQuoteBolt11Response` with type Q (generic/unknown) to a
275    /// `MeltQuoteBolt11Response` with `String`
276    pub fn to_string_id(self) -> MeltQuoteBolt11Response<String> {
277        MeltQuoteBolt11Response {
278            quote: self.quote.to_string(),
279            amount: self.amount,
280            fee_reserve: self.fee_reserve,
281            state: self.state,
282            expiry: self.expiry,
283            payment_preimage: self.payment_preimage,
284            change: self.change,
285            request: self.request,
286            unit: self.unit,
287        }
288    }
289}
290
291#[cfg(feature = "mint")]
292impl From<MeltQuoteBolt11Response<QuoteId>> for MeltQuoteBolt11Response<String> {
293    fn from(value: MeltQuoteBolt11Response<QuoteId>) -> Self {
294        Self {
295            quote: value.quote.to_string(),
296            amount: value.amount,
297            fee_reserve: value.fee_reserve,
298            state: value.state,
299            expiry: value.expiry,
300            payment_preimage: value.payment_preimage,
301            change: value.change,
302            request: value.request,
303            unit: value.unit,
304        }
305    }
306}