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