Skip to main content

cdk_common/
mint.rs

1//! Mint types
2
3use std::fmt;
4use std::ops::Deref;
5use std::str::FromStr;
6
7use bitcoin::bip32::DerivationPath;
8use cashu::quote_id::QuoteId;
9use cashu::util::unix_time;
10use cashu::{
11    Bolt11Invoice, MeltOptions, MeltQuoteBolt11Response, MintQuoteBolt11Response,
12    MintQuoteBolt12Response, PaymentMethod, Proofs, State,
13};
14use lightning::offers::offer::Offer;
15use serde::{Deserialize, Serialize};
16use tracing::instrument;
17use uuid::Uuid;
18
19use crate::nuts::{MeltQuoteState, MintQuoteState};
20use crate::payment::PaymentIdentifier;
21use crate::{Amount, CurrencyUnit, Error, Id, KeySetInfo, PublicKey};
22
23/// Operation kind for saga persistence
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26pub enum OperationKind {
27    /// Swap operation
28    Swap,
29    /// Mint operation
30    Mint,
31    /// Melt operation
32    Melt,
33}
34
35/// A collection of proofs that share a common state.
36///
37/// This type enforces the invariant that all proofs in the collection have the same state.
38/// The mint never needs to operate on a set of proofs with different states - proofs are
39/// always processed together as a unit (e.g., during swap, melt, or mint operations).
40///
41/// # Database Layer Responsibility
42///
43/// This design shifts the responsibility of ensuring state consistency to the database layer.
44/// When the database retrieves proofs via [`get_proofs`](crate::database::mint::ProofsTransaction::get_proofs),
45/// it must verify that all requested proofs share the same state and return an error if they don't.
46/// This prevents invalid proof sets from propagating through the system.
47///
48/// # State Transitions
49///
50/// State transitions are validated using [`check_state_transition`](crate::state::check_state_transition)
51/// before updating. The database layer then persists the new state for all proofs in a single transaction
52/// via [`update_proofs_state`](crate::database::mint::ProofsTransaction::update_proofs_state).
53///
54/// # Example
55///
56/// ```ignore
57/// // Database layer ensures all proofs have the same state
58/// let mut proofs = tx.get_proofs(&ys).await?;
59///
60/// // Validate the state transition
61/// check_state_transition(proofs.state, State::Spent)?;
62///
63/// // Persist the state change
64/// tx.update_proofs_state(&mut proofs, State::Spent).await?;
65/// ```
66#[derive(Debug)]
67pub struct ProofsWithState {
68    proofs: Proofs,
69    /// The current state of the proofs
70    pub state: State,
71}
72
73impl Deref for ProofsWithState {
74    type Target = Proofs;
75
76    fn deref(&self) -> &Self::Target {
77        &self.proofs
78    }
79}
80
81impl ProofsWithState {
82    /// Creates a new `ProofsWithState` with the given proofs and their shared state.
83    ///
84    /// # Note
85    ///
86    /// This constructor assumes all proofs share the given state. It is typically
87    /// called by the database layer after verifying state consistency.
88    pub fn new(proofs: Proofs, current_state: State) -> Self {
89        Self {
90            proofs,
91            state: current_state,
92        }
93    }
94}
95
96impl fmt::Display for OperationKind {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match self {
99            OperationKind::Swap => write!(f, "swap"),
100            OperationKind::Mint => write!(f, "mint"),
101            OperationKind::Melt => write!(f, "melt"),
102        }
103    }
104}
105
106impl FromStr for OperationKind {
107    type Err = Error;
108    fn from_str(value: &str) -> Result<Self, Self::Err> {
109        let value = value.to_lowercase();
110        match value.as_str() {
111            "swap" => Ok(OperationKind::Swap),
112            "mint" => Ok(OperationKind::Mint),
113            "melt" => Ok(OperationKind::Melt),
114            _ => Err(Error::Custom(format!("Invalid operation kind: {value}"))),
115        }
116    }
117}
118
119/// States specific to swap saga
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "snake_case")]
122pub enum SwapSagaState {
123    /// Swap setup complete (proofs added, blinded messages added)
124    SetupComplete,
125    /// Outputs signed (signatures generated but not persisted)
126    Signed,
127}
128
129impl fmt::Display for SwapSagaState {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self {
132            SwapSagaState::SetupComplete => write!(f, "setup_complete"),
133            SwapSagaState::Signed => write!(f, "signed"),
134        }
135    }
136}
137
138impl FromStr for SwapSagaState {
139    type Err = Error;
140    fn from_str(value: &str) -> Result<Self, Self::Err> {
141        let value = value.to_lowercase();
142        match value.as_str() {
143            "setup_complete" => Ok(SwapSagaState::SetupComplete),
144            "signed" => Ok(SwapSagaState::Signed),
145            _ => Err(Error::Custom(format!("Invalid swap saga state: {value}"))),
146        }
147    }
148}
149
150/// States specific to melt saga
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum MeltSagaState {
154    /// Setup complete (proofs reserved, quote verified)
155    SetupComplete,
156    /// Payment attempted to Lightning network (may or may not have succeeded)
157    PaymentAttempted,
158    /// TX1 committed (proofs Spent, quote Paid) - change signing + cleanup pending
159    Finalizing,
160}
161
162impl fmt::Display for MeltSagaState {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        match self {
165            MeltSagaState::SetupComplete => write!(f, "setup_complete"),
166            MeltSagaState::PaymentAttempted => write!(f, "payment_attempted"),
167            MeltSagaState::Finalizing => write!(f, "finalizing"),
168        }
169    }
170}
171
172impl FromStr for MeltSagaState {
173    type Err = Error;
174    fn from_str(value: &str) -> Result<Self, Self::Err> {
175        let value = value.to_lowercase();
176        match value.as_str() {
177            "setup_complete" => Ok(MeltSagaState::SetupComplete),
178            "payment_attempted" => Ok(MeltSagaState::PaymentAttempted),
179            "finalizing" => Ok(MeltSagaState::Finalizing),
180            _ => Err(Error::Custom(format!("Invalid melt saga state: {}", value))),
181        }
182    }
183}
184
185/// Saga state for different operation types
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(tag = "type", rename_all = "snake_case")]
188pub enum SagaStateEnum {
189    /// Swap saga states
190    Swap(SwapSagaState),
191    /// Melt saga states
192    Melt(MeltSagaState),
193    // Future: Mint saga states
194    // Mint(MintSagaState),
195}
196
197impl SagaStateEnum {
198    /// Create from string given operation kind
199    pub fn new(operation_kind: OperationKind, s: &str) -> Result<Self, Error> {
200        match operation_kind {
201            OperationKind::Swap => Ok(SagaStateEnum::Swap(SwapSagaState::from_str(s)?)),
202            OperationKind::Melt => Ok(SagaStateEnum::Melt(MeltSagaState::from_str(s)?)),
203            OperationKind::Mint => Err(Error::Custom("Mint saga not implemented yet".to_string())),
204        }
205    }
206
207    /// Get string representation of the state
208    pub fn state(&self) -> &str {
209        match self {
210            SagaStateEnum::Swap(state) => match state {
211                SwapSagaState::SetupComplete => "setup_complete",
212                SwapSagaState::Signed => "signed",
213            },
214            SagaStateEnum::Melt(state) => match state {
215                MeltSagaState::SetupComplete => "setup_complete",
216                MeltSagaState::PaymentAttempted => "payment_attempted",
217                MeltSagaState::Finalizing => "finalizing",
218            },
219        }
220    }
221}
222
223/// Persisted saga for recovery
224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225pub struct Saga {
226    /// Operation ID (correlation key)
227    pub operation_id: Uuid,
228    /// Operation kind (swap, mint, melt)
229    pub operation_kind: OperationKind,
230    /// Current saga state (operation-specific)
231    pub state: SagaStateEnum,
232    /// Quote ID for melt operations (used for payment status lookup during recovery)
233    /// None for swap operations
234    pub quote_id: Option<String>,
235    /// Unix timestamp when saga was created
236    pub created_at: u64,
237    /// Unix timestamp when saga was last updated
238    pub updated_at: u64,
239}
240
241impl Saga {
242    /// Create new swap saga
243    pub fn new_swap(operation_id: Uuid, state: SwapSagaState) -> Self {
244        let now = unix_time();
245        Self {
246            operation_id,
247            operation_kind: OperationKind::Swap,
248            state: SagaStateEnum::Swap(state),
249            quote_id: None,
250            created_at: now,
251            updated_at: now,
252        }
253    }
254
255    /// Update swap saga state
256    pub fn update_swap_state(&mut self, new_state: SwapSagaState) {
257        self.state = SagaStateEnum::Swap(new_state);
258        self.updated_at = unix_time();
259    }
260
261    /// Create new melt saga
262    pub fn new_melt(operation_id: Uuid, state: MeltSagaState, quote_id: String) -> Self {
263        let now = unix_time();
264        Self {
265            operation_id,
266            operation_kind: OperationKind::Melt,
267            state: SagaStateEnum::Melt(state),
268            quote_id: Some(quote_id),
269            created_at: now,
270            updated_at: now,
271        }
272    }
273
274    /// Update melt saga state
275    pub fn update_melt_state(&mut self, new_state: MeltSagaState) {
276        self.state = SagaStateEnum::Melt(new_state);
277        self.updated_at = unix_time();
278    }
279}
280
281/// Operation
282#[derive(Debug)]
283pub struct Operation {
284    id: Uuid,
285    kind: OperationKind,
286    total_issued: Amount,
287    total_redeemed: Amount,
288    fee_collected: Amount,
289    complete_at: Option<u64>,
290    /// Payment amount (only for melt operations)
291    payment_amount: Option<Amount>,
292    /// Payment fee (only for melt operations)
293    payment_fee: Option<Amount>,
294    /// Payment method (only for mint/melt operations)
295    payment_method: Option<PaymentMethod>,
296}
297
298impl Operation {
299    /// New
300    pub fn new(
301        id: Uuid,
302        kind: OperationKind,
303        total_issued: Amount,
304        total_redeemed: Amount,
305        fee_collected: Amount,
306        complete_at: Option<u64>,
307        payment_method: Option<PaymentMethod>,
308    ) -> Self {
309        Self {
310            id,
311            kind,
312            total_issued,
313            total_redeemed,
314            fee_collected,
315            complete_at,
316            payment_amount: None,
317            payment_fee: None,
318            payment_method,
319        }
320    }
321
322    /// Mint
323    pub fn new_mint(total_issued: Amount, payment_method: PaymentMethod) -> Self {
324        Self {
325            id: Uuid::new_v4(),
326            kind: OperationKind::Mint,
327            total_issued,
328            total_redeemed: Amount::ZERO,
329            fee_collected: Amount::ZERO,
330            complete_at: None,
331            payment_amount: None,
332            payment_fee: None,
333            payment_method: Some(payment_method),
334        }
335    }
336    /// Melt
337    ///
338    /// In the context of a melt total_issued refrests to the change
339    pub fn new_melt(
340        total_redeemed: Amount,
341        fee_collected: Amount,
342        payment_method: PaymentMethod,
343    ) -> Self {
344        Self {
345            id: Uuid::new_v4(),
346            kind: OperationKind::Melt,
347            total_issued: Amount::ZERO,
348            total_redeemed,
349            fee_collected,
350            complete_at: None,
351            payment_amount: None,
352            payment_fee: None,
353            payment_method: Some(payment_method),
354        }
355    }
356
357    /// Swap
358    pub fn new_swap(total_issued: Amount, total_redeemed: Amount, fee_collected: Amount) -> Self {
359        Self {
360            id: Uuid::new_v4(),
361            kind: OperationKind::Swap,
362            total_issued,
363            total_redeemed,
364            fee_collected,
365            complete_at: None,
366            payment_amount: None,
367            payment_fee: None,
368            payment_method: None,
369        }
370    }
371
372    /// Operation id
373    pub fn id(&self) -> &Uuid {
374        &self.id
375    }
376
377    /// Operation kind
378    pub fn kind(&self) -> OperationKind {
379        self.kind
380    }
381
382    /// Total issued
383    pub fn total_issued(&self) -> Amount {
384        self.total_issued
385    }
386
387    /// Total redeemed
388    pub fn total_redeemed(&self) -> Amount {
389        self.total_redeemed
390    }
391
392    /// Fee collected
393    pub fn fee_collected(&self) -> Amount {
394        self.fee_collected
395    }
396
397    /// Completed time
398    pub fn completed_at(&self) -> &Option<u64> {
399        &self.complete_at
400    }
401
402    /// Add change
403    pub fn add_change(&mut self, change: Amount) {
404        self.total_issued = change;
405    }
406
407    /// Payment amount (only for melt operations)
408    pub fn payment_amount(&self) -> Option<Amount> {
409        self.payment_amount
410    }
411
412    /// Payment fee (only for melt operations)
413    pub fn payment_fee(&self) -> Option<Amount> {
414        self.payment_fee
415    }
416
417    /// Set payment details for melt operations
418    pub fn set_payment_details(&mut self, payment_amount: Amount, payment_fee: Amount) {
419        self.payment_amount = Some(payment_amount);
420        self.payment_fee = Some(payment_fee);
421    }
422
423    /// Payment method (only for mint/melt operations)
424    pub fn payment_method(&self) -> Option<PaymentMethod> {
425        self.payment_method.clone()
426    }
427}
428
429/// Tracks pending changes made to a [`MintQuote`] that need to be persisted.
430///
431/// This struct implements a change-tracking pattern that separates domain logic from
432/// persistence concerns. When modifications are made to a `MintQuote` via methods like
433/// [`MintQuote::add_payment`] or [`MintQuote::add_issuance`], the changes are recorded
434/// here rather than being immediately persisted. The database layer can then call
435/// [`MintQuote::take_changes`] to retrieve and persist only the modifications.
436///
437/// This approach allows business rule validation to happen in the domain model while
438/// keeping the database layer focused purely on persistence.
439#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
440pub struct MintQuoteChange {
441    /// New payments added since the quote was loaded or last persisted.
442    pub payments: Option<Vec<IncomingPayment>>,
443    /// New issuance amounts recorded since the quote was loaded or last persisted.
444    pub issuances: Option<Vec<Amount>>,
445}
446
447/// Mint Quote Info
448#[derive(Debug, Clone, Hash, PartialEq, Eq)]
449pub struct MintQuote {
450    /// Quote id
451    pub id: QuoteId,
452    /// Amount of quote
453    pub amount: Option<Amount<CurrencyUnit>>,
454    /// Unit of quote
455    pub unit: CurrencyUnit,
456    /// Quote payment request e.g. bolt11
457    pub request: String,
458    /// Expiration time of quote
459    pub expiry: u64,
460    /// Value used by ln backend to look up state of request
461    pub request_lookup_id: PaymentIdentifier,
462    /// Pubkey
463    pub pubkey: Option<PublicKey>,
464    /// Unix time quote was created
465    pub created_time: u64,
466    /// Amount paid (typed for type safety)
467    amount_paid: Amount<CurrencyUnit>,
468    /// Amount issued (typed for type safety)
469    amount_issued: Amount<CurrencyUnit>,
470    /// Payment of payment(s) that filled quote
471    pub payments: Vec<IncomingPayment>,
472    /// Payment Method
473    pub payment_method: PaymentMethod,
474    /// Payment of payment(s) that filled quote
475    pub issuance: Vec<Issuance>,
476    /// Extra payment-method-specific fields
477    pub extra_json: Option<serde_json::Value>,
478    /// Accumulated changes since this quote was loaded or created.
479    ///
480    /// This field is not serialized and is used internally to track modifications
481    /// that need to be persisted. Use [`Self::take_changes`] to extract pending
482    /// changes for persistence.
483    changes: Option<MintQuoteChange>,
484}
485
486impl MintQuote {
487    /// Create new [`MintQuote`]
488    #[allow(clippy::too_many_arguments)]
489    pub fn new(
490        id: Option<QuoteId>,
491        request: String,
492        unit: CurrencyUnit,
493        amount: Option<Amount<CurrencyUnit>>,
494        expiry: u64,
495        request_lookup_id: PaymentIdentifier,
496        pubkey: Option<PublicKey>,
497        amount_paid: Amount<CurrencyUnit>,
498        amount_issued: Amount<CurrencyUnit>,
499        payment_method: PaymentMethod,
500        created_time: u64,
501        payments: Vec<IncomingPayment>,
502        issuance: Vec<Issuance>,
503        extra_json: Option<serde_json::Value>,
504    ) -> Self {
505        let id = id.unwrap_or_else(QuoteId::new_uuid);
506
507        Self {
508            id,
509            amount,
510            unit: unit.clone(),
511            request,
512            expiry,
513            request_lookup_id,
514            pubkey,
515            created_time,
516            amount_paid,
517            amount_issued,
518            payment_method,
519            payments,
520            issuance,
521            extra_json,
522            changes: None,
523        }
524    }
525
526    /// Increment the amount paid on the mint quote by a given amount
527    #[instrument(skip(self))]
528    pub fn increment_amount_paid(
529        &mut self,
530        additional_amount: Amount<CurrencyUnit>,
531    ) -> Result<Amount, crate::Error> {
532        self.amount_paid = self
533            .amount_paid
534            .checked_add(&additional_amount)
535            .map_err(|_| crate::Error::AmountOverflow)?;
536        Ok(Amount::from(self.amount_paid.value()))
537    }
538
539    /// Amount paid
540    #[instrument(skip(self))]
541    pub fn amount_paid(&self) -> Amount<CurrencyUnit> {
542        self.amount_paid.clone()
543    }
544
545    /// Records tokens being issued against this mint quote.
546    ///
547    /// This method validates that the issuance doesn't exceed the amount paid, updates
548    /// the quote's internal state, and records the change for later persistence. The
549    /// `amount_issued` counter is incremented and the issuance is added to the change
550    /// tracker for the database layer to persist.
551    ///
552    /// # Arguments
553    ///
554    /// * `additional_amount` - The amount of tokens being issued.
555    ///
556    /// # Returns
557    ///
558    /// Returns the new total `amount_issued` after this issuance is recorded.
559    ///
560    /// # Errors
561    ///
562    /// Returns [`crate::Error::OverIssue`] if the new issued amount would exceed the
563    /// amount paid (cannot issue more tokens than have been paid for).
564    ///
565    /// Returns [`crate::Error::AmountOverflow`] if adding the issuance amount would
566    /// cause an arithmetic overflow.
567    #[instrument(skip(self))]
568    pub fn add_issuance(
569        &mut self,
570        additional_amount: Amount<CurrencyUnit>,
571    ) -> Result<Amount<CurrencyUnit>, crate::Error> {
572        let new_amount_issued = self
573            .amount_issued
574            .checked_add(&additional_amount)
575            .map_err(|_| crate::Error::AmountOverflow)?;
576
577        // Can't issue more than what's been paid
578        if new_amount_issued > self.amount_paid {
579            return Err(crate::Error::OverIssue);
580        }
581
582        self.changes
583            .get_or_insert_default()
584            .issuances
585            .get_or_insert_default()
586            .push(additional_amount.into());
587
588        self.amount_issued = new_amount_issued;
589
590        Ok(self.amount_issued.clone())
591    }
592
593    /// Amount issued
594    #[instrument(skip(self))]
595    pub fn amount_issued(&self) -> Amount<CurrencyUnit> {
596        self.amount_issued.clone()
597    }
598
599    /// Get state of mint quote
600    #[instrument(skip(self))]
601    pub fn state(&self) -> MintQuoteState {
602        self.compute_quote_state()
603    }
604
605    /// Existing payment ids of a mint quote
606    pub fn payment_ids(&self) -> Vec<&String> {
607        self.payments.iter().map(|a| &a.payment_id).collect()
608    }
609
610    /// Amount mintable
611    /// Returns the amount that is still available for minting.
612    ///
613    /// The value is computed as the difference between the total amount that
614    /// has been paid for this issuance (`self.amount_paid`) and the amount
615    /// that has already been issued (`self.amount_issued`). In other words,
616    pub fn amount_mintable(&self) -> Amount<CurrencyUnit> {
617        self.amount_paid
618            .checked_sub(&self.amount_issued)
619            .unwrap_or_else(|_| Amount::new(0, self.unit.clone()))
620    }
621
622    /// Extracts and returns all pending changes, leaving the internal change tracker empty.
623    ///
624    /// This method is typically called by the database layer after loading or modifying a quote. It
625    /// returns any accumulated changes (new payments, issuances) that need to be persisted, and
626    /// clears the internal change buffer so that subsequent calls return `None` until new
627    /// modifications are made.
628    ///
629    /// Returns `None` if no changes have been made since the last call to this method or since the
630    /// quote was created/loaded.
631    pub fn take_changes(&mut self) -> Option<MintQuoteChange> {
632        self.changes.take()
633    }
634
635    /// Records a new payment received for this mint quote.
636    ///
637    /// This method validates the payment, updates the quote's internal state, and records the
638    /// change for later persistence. The `amount_paid` counter is incremented and the payment is
639    /// added to the change tracker for the database layer to persist.
640    ///
641    /// # Arguments
642    ///
643    /// * `amount` - The amount of the payment in the quote's currency unit. * `payment_id` - A
644    /// unique identifier for this payment (e.g., lightning payment hash). * `time` - Optional Unix
645    /// timestamp of when the payment was received. If `None`, the current time is used.
646    ///
647    /// # Errors
648    ///
649    /// Returns [`crate::Error::DuplicatePaymentId`] if a payment with the same ID has already been
650    /// recorded for this quote.
651    ///
652    /// Returns [`crate::Error::AmountOverflow`] if adding the payment amount would cause an
653    /// arithmetic overflow.
654    #[instrument(skip(self))]
655    pub fn add_payment(
656        &mut self,
657        amount: Amount<CurrencyUnit>,
658        payment_id: String,
659        time: Option<u64>,
660    ) -> Result<(), crate::Error> {
661        let time = time.unwrap_or_else(unix_time);
662
663        let payment_ids = self.payment_ids();
664        if payment_ids.contains(&&payment_id) {
665            return Err(crate::Error::DuplicatePaymentId);
666        }
667
668        self.amount_paid = self
669            .amount_paid
670            .checked_add(&amount)
671            .map_err(|_| crate::Error::AmountOverflow)?;
672
673        let payment = IncomingPayment::new(amount, payment_id, time);
674
675        self.payments.push(payment.clone());
676
677        self.changes
678            .get_or_insert_default()
679            .payments
680            .get_or_insert_default()
681            .push(payment);
682
683        Ok(())
684    }
685
686    /// Compute quote state
687    #[instrument(skip(self))]
688    fn compute_quote_state(&self) -> MintQuoteState {
689        let zero_amount = Amount::new(0, self.unit.clone());
690
691        if self.amount_paid == zero_amount && self.amount_issued == zero_amount {
692            return MintQuoteState::Unpaid;
693        }
694
695        match self.amount_paid.value().cmp(&self.amount_issued.value()) {
696            std::cmp::Ordering::Less => {
697                tracing::error!("We should not have issued more then has been paid");
698                MintQuoteState::Issued
699            }
700            std::cmp::Ordering::Equal => MintQuoteState::Issued,
701            std::cmp::Ordering::Greater => MintQuoteState::Paid,
702        }
703    }
704}
705
706/// Mint Payments
707#[derive(Debug, Clone, Hash, PartialEq, Eq)]
708pub struct IncomingPayment {
709    /// Amount
710    pub amount: Amount<CurrencyUnit>,
711    /// Pyament unix time
712    pub time: u64,
713    /// Payment id
714    pub payment_id: String,
715}
716
717impl IncomingPayment {
718    /// New [`IncomingPayment`]
719    pub fn new(amount: Amount<CurrencyUnit>, payment_id: String, time: u64) -> Self {
720        Self {
721            payment_id,
722            time,
723            amount,
724        }
725    }
726}
727
728/// Information about issued quote
729#[derive(Debug, Clone, Hash, PartialEq, Eq)]
730pub struct Issuance {
731    /// Amount
732    pub amount: Amount<CurrencyUnit>,
733    /// Time
734    pub time: u64,
735}
736
737impl Issuance {
738    /// Create new [`Issuance`]
739    pub fn new(amount: Amount<CurrencyUnit>, time: u64) -> Self {
740        Self { amount, time }
741    }
742}
743
744/// Melt Quote Info
745#[derive(Debug, Clone, Hash, PartialEq, Eq)]
746pub struct MeltQuote {
747    /// Quote id
748    pub id: QuoteId,
749    /// Quote unit
750    pub unit: CurrencyUnit,
751    /// Quote Payment request e.g. bolt11
752    pub request: MeltPaymentRequest,
753    /// Quote amount (typed for type safety)
754    amount: Amount<CurrencyUnit>,
755    /// Quote fee reserve (typed for type safety)
756    fee_reserve: Amount<CurrencyUnit>,
757    /// Quote state
758    pub state: MeltQuoteState,
759    /// Expiration time of quote
760    pub expiry: u64,
761    /// Payment preimage
762    pub payment_preimage: Option<String>,
763    /// Value used by ln backend to look up state of request
764    pub request_lookup_id: Option<PaymentIdentifier>,
765    /// Payment options
766    ///
767    /// Used for amountless invoices and MPP payments
768    pub options: Option<MeltOptions>,
769    /// Unix time quote was created
770    pub created_time: u64,
771    /// Unix time quote was paid
772    pub paid_time: Option<u64>,
773    /// Payment method
774    pub payment_method: PaymentMethod,
775}
776
777impl MeltQuote {
778    /// Create new [`MeltQuote`]
779    #[allow(clippy::too_many_arguments)]
780    pub fn new(
781        request: MeltPaymentRequest,
782        unit: CurrencyUnit,
783        amount: Amount<CurrencyUnit>,
784        fee_reserve: Amount<CurrencyUnit>,
785        expiry: u64,
786        request_lookup_id: Option<PaymentIdentifier>,
787        options: Option<MeltOptions>,
788        payment_method: PaymentMethod,
789    ) -> Self {
790        let id = Uuid::new_v4();
791
792        Self {
793            id: QuoteId::UUID(id),
794            unit: unit.clone(),
795            request,
796            amount,
797            fee_reserve,
798            state: MeltQuoteState::Unpaid,
799            expiry,
800            payment_preimage: None,
801            request_lookup_id,
802            options,
803            created_time: unix_time(),
804            paid_time: None,
805            payment_method,
806        }
807    }
808
809    /// Quote amount
810    #[inline]
811    pub fn amount(&self) -> Amount<CurrencyUnit> {
812        self.amount.clone()
813    }
814
815    /// Fee reserve
816    #[inline]
817    pub fn fee_reserve(&self) -> Amount<CurrencyUnit> {
818        self.fee_reserve.clone()
819    }
820
821    /// Total amount needed (amount + fee_reserve)
822    pub fn total_needed(&self) -> Result<Amount, crate::Error> {
823        let total = self
824            .amount
825            .checked_add(&self.fee_reserve)
826            .map_err(|_| crate::Error::AmountOverflow)?;
827        Ok(Amount::from(total.value()))
828    }
829
830    /// Create MeltQuote from database fields (for deserialization)
831    #[allow(clippy::too_many_arguments)]
832    pub fn from_db(
833        id: QuoteId,
834        unit: CurrencyUnit,
835        request: MeltPaymentRequest,
836        amount: u64,
837        fee_reserve: u64,
838        state: MeltQuoteState,
839        expiry: u64,
840        payment_preimage: Option<String>,
841        request_lookup_id: Option<PaymentIdentifier>,
842        options: Option<MeltOptions>,
843        created_time: u64,
844        paid_time: Option<u64>,
845        payment_method: PaymentMethod,
846    ) -> Self {
847        Self {
848            id,
849            unit: unit.clone(),
850            request,
851            amount: Amount::new(amount, unit.clone()),
852            fee_reserve: Amount::new(fee_reserve, unit),
853            state,
854            expiry,
855            payment_preimage,
856            request_lookup_id,
857            options,
858            created_time,
859            paid_time,
860            payment_method,
861        }
862    }
863}
864
865/// Mint Keyset Info
866#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
867pub struct MintKeySetInfo {
868    /// Keyset [`Id`]
869    pub id: Id,
870    /// Keyset [`CurrencyUnit`]
871    pub unit: CurrencyUnit,
872    /// Keyset active or inactive
873    /// Mint will only issue new signatures on active keysets
874    pub active: bool,
875    /// Starting unix time Keyset is valid from
876    pub valid_from: u64,
877    /// [`DerivationPath`] keyset
878    pub derivation_path: DerivationPath,
879    /// DerivationPath index of Keyset
880    pub derivation_path_index: Option<u32>,
881    /// Supported amounts
882    pub amounts: Vec<u64>,
883    /// Input Fee ppk
884    #[serde(default = "default_fee")]
885    pub input_fee_ppk: u64,
886    /// Final expiry
887    pub final_expiry: Option<u64>,
888}
889
890/// Default fee
891pub fn default_fee() -> u64 {
892    0
893}
894
895impl From<MintKeySetInfo> for KeySetInfo {
896    fn from(keyset_info: MintKeySetInfo) -> Self {
897        Self {
898            id: keyset_info.id,
899            unit: keyset_info.unit,
900            active: keyset_info.active,
901            input_fee_ppk: keyset_info.input_fee_ppk,
902            final_expiry: keyset_info.final_expiry,
903        }
904    }
905}
906
907impl From<MintQuote> for MintQuoteBolt11Response<QuoteId> {
908    fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response<QuoteId> {
909        MintQuoteBolt11Response {
910            quote: mint_quote.id.clone(),
911            state: mint_quote.state(),
912            request: mint_quote.request,
913            expiry: Some(mint_quote.expiry),
914            pubkey: mint_quote.pubkey,
915            amount: mint_quote.amount.map(Into::into),
916            unit: Some(mint_quote.unit.clone()),
917        }
918    }
919}
920
921impl From<MintQuote> for MintQuoteBolt11Response<String> {
922    fn from(quote: MintQuote) -> Self {
923        let quote: MintQuoteBolt11Response<QuoteId> = quote.into();
924
925        quote.into()
926    }
927}
928
929impl TryFrom<crate::mint::MintQuote> for MintQuoteBolt12Response<QuoteId> {
930    type Error = crate::Error;
931
932    fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
933        Ok(MintQuoteBolt12Response {
934            quote: mint_quote.id.clone(),
935            request: mint_quote.request,
936            expiry: Some(mint_quote.expiry),
937            amount_paid: Amount::from(mint_quote.amount_paid.value()),
938            amount_issued: Amount::from(mint_quote.amount_issued.value()),
939            pubkey: mint_quote.pubkey.ok_or(crate::Error::PubkeyRequired)?,
940            amount: mint_quote.amount.map(Into::into),
941            unit: mint_quote.unit,
942        })
943    }
944}
945
946impl TryFrom<MintQuote> for MintQuoteBolt12Response<String> {
947    type Error = crate::Error;
948
949    fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
950        let quote: MintQuoteBolt12Response<QuoteId> = quote.try_into()?;
951
952        Ok(quote.into())
953    }
954}
955
956impl TryFrom<crate::mint::MintQuote> for crate::nuts::MintQuoteCustomResponse<QuoteId> {
957    type Error = crate::Error;
958
959    fn try_from(mint_quote: crate::mint::MintQuote) -> Result<Self, Self::Error> {
960        Ok(crate::nuts::MintQuoteCustomResponse {
961            state: mint_quote.state(),
962            quote: mint_quote.id.clone(),
963            request: mint_quote.request,
964            expiry: Some(mint_quote.expiry),
965            pubkey: mint_quote.pubkey,
966            amount: mint_quote.amount.map(Into::into),
967            unit: Some(mint_quote.unit),
968            extra: mint_quote.extra_json.unwrap_or_default(),
969        })
970    }
971}
972
973impl TryFrom<MintQuote> for crate::nuts::MintQuoteCustomResponse<String> {
974    type Error = crate::Error;
975
976    fn try_from(quote: MintQuote) -> Result<Self, Self::Error> {
977        let quote: crate::nuts::MintQuoteCustomResponse<QuoteId> = quote.try_into()?;
978
979        Ok(quote.into())
980    }
981}
982
983impl From<&MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
984    fn from(melt_quote: &MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
985        MeltQuoteBolt11Response {
986            quote: melt_quote.id.clone(),
987            payment_preimage: None,
988            change: None,
989            state: melt_quote.state,
990            expiry: melt_quote.expiry,
991            amount: melt_quote.amount().clone().into(),
992            fee_reserve: melt_quote.fee_reserve().clone().into(),
993            request: None,
994            unit: Some(melt_quote.unit.clone()),
995        }
996    }
997}
998
999impl From<MeltQuote> for MeltQuoteBolt11Response<QuoteId> {
1000    fn from(melt_quote: MeltQuote) -> MeltQuoteBolt11Response<QuoteId> {
1001        MeltQuoteBolt11Response {
1002            quote: melt_quote.id.clone(),
1003            amount: melt_quote.amount().clone().into(),
1004            fee_reserve: melt_quote.fee_reserve().clone().into(),
1005            state: melt_quote.state,
1006            expiry: melt_quote.expiry,
1007            payment_preimage: melt_quote.payment_preimage,
1008            change: None,
1009            request: Some(melt_quote.request.to_string()),
1010            unit: Some(melt_quote.unit.clone()),
1011        }
1012    }
1013}
1014
1015/// Payment request
1016#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
1017pub enum MeltPaymentRequest {
1018    /// Bolt11 Payment
1019    Bolt11 {
1020        /// Bolt11 invoice
1021        bolt11: Bolt11Invoice,
1022    },
1023    /// Bolt12 Payment
1024    Bolt12 {
1025        /// Offer
1026        #[serde(with = "offer_serde")]
1027        offer: Box<Offer>,
1028    },
1029    /// Custom payment method
1030    Custom {
1031        /// Payment method name
1032        method: String,
1033        /// Payment request string
1034        request: String,
1035    },
1036}
1037
1038impl std::fmt::Display for MeltPaymentRequest {
1039    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1040        match self {
1041            MeltPaymentRequest::Bolt11 { bolt11 } => write!(f, "{bolt11}"),
1042            MeltPaymentRequest::Bolt12 { offer } => write!(f, "{offer}"),
1043            MeltPaymentRequest::Custom { request, .. } => write!(f, "{request}"),
1044        }
1045    }
1046}
1047
1048mod offer_serde {
1049    use std::str::FromStr;
1050
1051    use serde::{self, Deserialize, Deserializer, Serializer};
1052
1053    use super::Offer;
1054
1055    pub fn serialize<S>(offer: &Offer, serializer: S) -> Result<S::Ok, S::Error>
1056    where
1057        S: Serializer,
1058    {
1059        let s = offer.to_string();
1060        serializer.serialize_str(&s)
1061    }
1062
1063    pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<Offer>, D::Error>
1064    where
1065        D: Deserializer<'de>,
1066    {
1067        let s = String::deserialize(deserializer)?;
1068        Ok(Box::new(Offer::from_str(&s).map_err(|_| {
1069            serde::de::Error::custom("Invalid Bolt12 Offer")
1070        })?))
1071    }
1072}