cdk_common/
mint.rs

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