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