cdk_common/
mint.rs

1//! Mint types
2
3use bitcoin::bip32::DerivationPath;
4use cashu::quote_id::QuoteId;
5use cashu::util::unix_time;
6use cashu::{
7    Bolt11Invoice, MeltOptions, MeltQuoteBolt11Response, MintQuoteBolt11Response,
8    MintQuoteBolt12Response, PaymentMethod,
9};
10use lightning::offers::offer::Offer;
11use serde::{Deserialize, Serialize};
12use tracing::instrument;
13use uuid::Uuid;
14
15use crate::nuts::{MeltQuoteState, MintQuoteState};
16use crate::payment::PaymentIdentifier;
17use crate::{Amount, CurrencyUnit, Id, KeySetInfo, PublicKey};
18
19/// Mint Quote Info
20#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
21pub struct MintQuote {
22    /// Quote id
23    pub id: QuoteId,
24    /// Amount of quote
25    pub amount: Option<Amount>,
26    /// Unit of quote
27    pub unit: CurrencyUnit,
28    /// Quote payment request e.g. bolt11
29    pub request: String,
30    /// Expiration time of quote
31    pub expiry: u64,
32    /// Value used by ln backend to look up state of request
33    pub request_lookup_id: PaymentIdentifier,
34    /// Pubkey
35    pub pubkey: Option<PublicKey>,
36    /// Unix time quote was created
37    #[serde(default)]
38    pub created_time: u64,
39    /// Amount paid
40    #[serde(default)]
41    amount_paid: Amount,
42    /// Amount issued
43    #[serde(default)]
44    amount_issued: Amount,
45    /// Payment of payment(s) that filled quote
46    #[serde(default)]
47    pub payments: Vec<IncomingPayment>,
48    /// Payment Method
49    #[serde(default)]
50    pub payment_method: PaymentMethod,
51    /// Payment of payment(s) that filled quote
52    #[serde(default)]
53    pub issuance: Vec<Issuance>,
54}
55
56impl MintQuote {
57    /// Create new [`MintQuote`]
58    #[allow(clippy::too_many_arguments)]
59    pub fn new(
60        id: Option<QuoteId>,
61        request: String,
62        unit: CurrencyUnit,
63        amount: Option<Amount>,
64        expiry: u64,
65        request_lookup_id: PaymentIdentifier,
66        pubkey: Option<PublicKey>,
67        amount_paid: Amount,
68        amount_issued: Amount,
69        payment_method: PaymentMethod,
70        created_time: u64,
71        payments: Vec<IncomingPayment>,
72        issuance: Vec<Issuance>,
73    ) -> Self {
74        let id = id.unwrap_or_else(QuoteId::new_uuid);
75
76        Self {
77            id,
78            amount,
79            unit,
80            request,
81            expiry,
82            request_lookup_id,
83            pubkey,
84            created_time,
85            amount_paid,
86            amount_issued,
87            payment_method,
88            payments,
89            issuance,
90        }
91    }
92
93    /// Increment the amount paid on the mint quote by a given amount
94    #[instrument(skip(self))]
95    pub fn increment_amount_paid(
96        &mut self,
97        additional_amount: Amount,
98    ) -> Result<Amount, crate::Error> {
99        self.amount_paid = self
100            .amount_paid
101            .checked_add(additional_amount)
102            .ok_or(crate::Error::AmountOverflow)?;
103        Ok(self.amount_paid)
104    }
105
106    /// Amount paid
107    #[instrument(skip(self))]
108    pub fn amount_paid(&self) -> Amount {
109        self.amount_paid
110    }
111
112    /// Increment the amount issued on the mint quote by a given amount
113    #[instrument(skip(self))]
114    pub fn increment_amount_issued(
115        &mut self,
116        additional_amount: Amount,
117    ) -> Result<Amount, crate::Error> {
118        self.amount_issued = self
119            .amount_issued
120            .checked_add(additional_amount)
121            .ok_or(crate::Error::AmountOverflow)?;
122        Ok(self.amount_issued)
123    }
124
125    /// Amount issued
126    #[instrument(skip(self))]
127    pub fn amount_issued(&self) -> Amount {
128        self.amount_issued
129    }
130
131    /// Get state of mint quote
132    #[instrument(skip(self))]
133    pub fn state(&self) -> MintQuoteState {
134        self.compute_quote_state()
135    }
136
137    /// Existing payment ids of a mint quote
138    pub fn payment_ids(&self) -> Vec<&String> {
139        self.payments.iter().map(|a| &a.payment_id).collect()
140    }
141
142    /// Amount mintable
143    /// Returns the amount that is still available for minting.
144    ///
145    /// The value is computed as the difference between the total amount that
146    /// has been paid for this issuance (`self.amount_paid`) and the amount
147    /// that has already been issued (`self.amount_issued`). In other words,
148    pub fn amount_mintable(&self) -> Amount {
149        self.amount_paid - self.amount_issued
150    }
151
152    /// Add a payment ID to the list of payment IDs
153    ///
154    /// Returns an error if the payment ID is already in the list
155    #[instrument(skip(self))]
156    pub fn add_payment(
157        &mut self,
158        amount: Amount,
159        payment_id: String,
160        time: u64,
161    ) -> Result<(), crate::Error> {
162        let payment_ids = self.payment_ids();
163        if payment_ids.contains(&&payment_id) {
164            return Err(crate::Error::DuplicatePaymentId);
165        }
166
167        let payment = IncomingPayment::new(amount, payment_id, time);
168
169        self.payments.push(payment);
170        Ok(())
171    }
172
173    /// Compute quote state
174    #[instrument(skip(self))]
175    fn compute_quote_state(&self) -> MintQuoteState {
176        if self.amount_paid == Amount::ZERO && self.amount_issued == Amount::ZERO {
177            return MintQuoteState::Unpaid;
178        }
179
180        match self.amount_paid.cmp(&self.amount_issued) {
181            std::cmp::Ordering::Less => {
182                // self.amount_paid is less than other (amount issued)
183                // Handle case where paid amount is insufficient
184                tracing::error!("We should not have issued more then has been paid");
185                MintQuoteState::Issued
186            }
187            std::cmp::Ordering::Equal => {
188                // We do this extra check for backwards compatibility for quotes where amount paid/issed was not tracked
189                // self.amount_paid equals other (amount issued)
190                // Handle case where paid amount exactly matches
191                MintQuoteState::Issued
192            }
193            std::cmp::Ordering::Greater => {
194                // self.amount_paid is greater than other (amount issued)
195                // Handle case where paid amount exceeds required amount
196                MintQuoteState::Paid
197            }
198        }
199    }
200}
201
202/// Mint Payments
203#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
204pub struct IncomingPayment {
205    /// Amount
206    pub amount: Amount,
207    /// Pyament unix time
208    pub time: u64,
209    /// Payment id
210    pub payment_id: String,
211}
212
213impl IncomingPayment {
214    /// New [`IncomingPayment`]
215    pub fn new(amount: Amount, payment_id: String, time: u64) -> Self {
216        Self {
217            payment_id,
218            time,
219            amount,
220        }
221    }
222}
223
224/// Informattion about issued quote
225#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
226pub struct Issuance {
227    /// Amount
228    pub amount: Amount,
229    /// Time
230    pub time: u64,
231}
232
233impl Issuance {
234    /// Create new [`Issuance`]
235    pub fn new(amount: Amount, time: u64) -> Self {
236        Self { amount, time }
237    }
238}
239
240/// Melt Quote Info
241#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
242pub struct MeltQuote {
243    /// Quote id
244    pub id: QuoteId,
245    /// Quote unit
246    pub unit: CurrencyUnit,
247    /// Quote amount
248    pub amount: Amount,
249    /// Quote Payment request e.g. bolt11
250    pub request: MeltPaymentRequest,
251    /// Quote fee reserve
252    pub fee_reserve: Amount,
253    /// Quote state
254    pub state: MeltQuoteState,
255    /// Expiration time of quote
256    pub expiry: u64,
257    /// Payment preimage
258    pub payment_preimage: Option<String>,
259    /// Value used by ln backend to look up state of request
260    pub request_lookup_id: Option<PaymentIdentifier>,
261    /// Payment options
262    ///
263    /// Used for amountless invoices and MPP payments
264    pub options: Option<MeltOptions>,
265    /// Unix time quote was created
266    #[serde(default)]
267    pub created_time: u64,
268    /// Unix time quote was paid
269    pub paid_time: Option<u64>,
270    /// Payment method
271    #[serde(default)]
272    pub payment_method: PaymentMethod,
273}
274
275impl MeltQuote {
276    /// Create new [`MeltQuote`]
277    #[allow(clippy::too_many_arguments)]
278    pub fn new(
279        request: MeltPaymentRequest,
280        unit: CurrencyUnit,
281        amount: Amount,
282        fee_reserve: Amount,
283        expiry: u64,
284        request_lookup_id: Option<PaymentIdentifier>,
285        options: Option<MeltOptions>,
286        payment_method: PaymentMethod,
287    ) -> Self {
288        let id = Uuid::new_v4();
289
290        Self {
291            id: QuoteId::UUID(id),
292            amount,
293            unit,
294            request,
295            fee_reserve,
296            state: MeltQuoteState::Unpaid,
297            expiry,
298            payment_preimage: None,
299            request_lookup_id,
300            options,
301            created_time: unix_time(),
302            paid_time: None,
303            payment_method,
304        }
305    }
306}
307
308/// Mint Keyset Info
309#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
310pub struct MintKeySetInfo {
311    /// Keyset [`Id`]
312    pub id: Id,
313    /// Keyset [`CurrencyUnit`]
314    pub unit: CurrencyUnit,
315    /// Keyset active or inactive
316    /// Mint will only issue new signatures on active keysets
317    pub active: bool,
318    /// Starting unix time Keyset is valid from
319    pub valid_from: u64,
320    /// [`DerivationPath`] keyset
321    pub derivation_path: DerivationPath,
322    /// DerivationPath index of Keyset
323    pub derivation_path_index: Option<u32>,
324    /// Max order of keyset
325    pub max_order: u8,
326    /// Supported amounts
327    pub amounts: Vec<u64>,
328    /// Input Fee ppk
329    #[serde(default = "default_fee")]
330    pub input_fee_ppk: u64,
331    /// Final expiry
332    pub final_expiry: Option<u64>,
333}
334
335/// Default fee
336pub fn default_fee() -> u64 {
337    0
338}
339
340impl From<MintKeySetInfo> for KeySetInfo {
341    fn from(keyset_info: MintKeySetInfo) -> Self {
342        Self {
343            id: keyset_info.id,
344            unit: keyset_info.unit,
345            active: keyset_info.active,
346            input_fee_ppk: keyset_info.input_fee_ppk,
347            final_expiry: keyset_info.final_expiry,
348        }
349    }
350}
351
352impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
353    fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<QuoteId> {
354        MintQuoteBolt11Response {
355            quote: mint_quote.id.clone(),
356            state: mint_quote.state(),
357            request: mint_quote.request,
358            expiry: Some(mint_quote.expiry),
359            pubkey: mint_quote.pubkey,
360            amount: mint_quote.amount,
361            unit: Some(mint_quote.unit.clone()),
362        }
363    }
364}
365
366impl From<MintQuote> for MintQuoteBolt11Response<String> {
367    fn from(quote: MintQuote) -> Self {
368        let quote: MintQuoteBolt11Response<QuoteId> = quote.into();
369
370        quote.into()
371    }
372}
373
374impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<QuoteId> {
375    type Error = crate::Error;
376
377    fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
378        Ok(MintQuoteBolt12Response {
379            quote: mint_quote.id.clone(),
380            request: mint_quote.request,
381            expiry: Some(mint_quote.expiry),
382            amount_paid: mint_quote.amount_paid,
383            amount_issued: mint_quote.amount_issued,
384            pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
385            amount: mint_quote.amount,
386            unit: mint_quote.unit,
387        })
388    }
389}
390
391impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
392    type Error = crate::Error;
393
394    fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
395        let quote: MintQuoteBolt12Response<QuoteId> = quote.try_into()?;
396
397        Ok(quote.into())
398    }
399}
400
401impl From<&MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
402    fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
403        MeltQuoteBolt11Response {
404            quote: melt_quote.id.clone(),
405            payment_preimage: None,
406            change: None,
407            state: melt_quote.state,
408            paid: Some(melt_quote.state == MeltQuoteState::Paid),
409            expiry: melt_quote.expiry,
410            amount: melt_quote.amount,
411            fee_reserve: melt_quote.fee_reserve,
412            request: None,
413            unit: Some(melt_quote.unit.clone()),
414        }
415    }
416}
417
418impl From<MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
419    fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
420        let paid = melt_quote.state == MeltQuoteState::Paid;
421        MeltQuoteBolt11Response {
422            quote: melt_quote.id.clone(),
423            amount: melt_quote.amount,
424            fee_reserve: melt_quote.fee_reserve,
425            paid: Some(paid),
426            state: melt_quote.state,
427            expiry: melt_quote.expiry,
428            payment_preimage: melt_quote.payment_preimage,
429            change: None,
430            request: Some(melt_quote.request.to_string()),
431            unit: Some(melt_quote.unit.clone()),
432        }
433    }
434}
435
436/// Payment request
437#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
438pub enum MeltPaymentRequest {
439    /// Bolt11 Payment
440    Bolt11 {
441        /// Bolt11 invoice
442        bolt11: Bolt11Invoice,
443    },
444    /// Bolt12 Payment
445    Bolt12 {
446        /// Offer
447        #[serde(with = "offer_serde")]
448        offer: Box<Offer>,
449    },
450}
451
452impl std::fmt::Display for MeltPaymentRequest {
453    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454        match self {
455            MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
456            MeltPaymentRequest::Bolt12 { offer } => write!(f, "{offer}"),
457        }
458    }
459}
460
461mod offer_serde {
462    use std::str::FromStr;
463
464    use serde::{self, Deserialize, Deserializer, Serializer};
465
466    use super::Offer;
467
468    pub fn serialize<S>(offer: &Offer, serializer: S) -> Result<S::Ok, S::Error>
469    where
470        S: Serializer,
471    {
472        let s = offer.to_string();
473        serializer.serialize_str(&s)
474    }
475
476    pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<Offer>, D::Error>
477    where
478        D: Deserializer<'de>,
479    {
480        let s = String::deserialize(deserializer)?;
481        Ok(Box::new(Offer::from_str(&s).map_err(|_| {
482            serde::de::Error::custom("Invalid Bolt12 Offer")
483        })?))
484    }
485}