Skip to main content

cdk_common/
payment.rs

1//! CDK mint payment backend interface
2
3use std::convert::Infallible;
4use std::fmt;
5use std::pin::Pin;
6
7use async_trait::async_trait;
8use cashu::util::hex;
9use cashu::{Bolt11Invoice, MeltOptions};
10#[cfg(feature = "prometheus")]
11use cdk_prometheus::MintMetricGuard;
12use futures::Stream;
13use lightning::offers::offer::Offer;
14use lightning_invoice::ParseOrSemanticError;
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use thiserror::Error;
18
19use crate::mint::{MeltPaymentRequest, MeltQuote};
20use crate::nuts::nut30::MeltQuoteOnchainFeeOption;
21use crate::nuts::{CurrencyUnit, MeltQuoteState, PublicKey};
22use crate::{Amount, QuoteId};
23
24/// CDK Payment Error
25#[derive(Debug, Error)]
26pub enum Error {
27    /// Invoice already paid
28    #[error("Invoice already paid")]
29    InvoiceAlreadyPaid,
30    /// Invoice pay pending
31    #[error("Invoice pay is pending")]
32    InvoicePaymentPending,
33    /// Unsupported unit
34    #[error("Unsupported unit")]
35    UnsupportedUnit,
36    /// Unsupported payment option
37    #[error("Unsupported payment option")]
38    UnsupportedPaymentOption,
39    /// Payment state is unknown
40    #[error("Payment state is unknown")]
41    UnknownPaymentState,
42    /// Amount mismatch
43    #[error("Amount is not what is expected")]
44    AmountMismatch,
45    /// Invalid expiry
46    #[error("Invalid expiry")]
47    InvalidExpiry,
48    /// Payment backend error
49    #[error(transparent)]
50    Backend(Box<dyn std::error::Error + Send + Sync>),
51    /// Onchain Error
52    #[error(transparent)]
53    Onchain(Box<dyn std::error::Error + Send + Sync>),
54    /// Serde Error
55    #[error(transparent)]
56    Serde(#[from] serde_json::Error),
57    /// AnyHow Error
58    #[error(transparent)]
59    Anyhow(#[from] anyhow::Error),
60    /// Parse Error
61    #[error(transparent)]
62    Parse(#[from] ParseOrSemanticError),
63    /// Amount Error
64    #[error(transparent)]
65    Amount(#[from] crate::amount::Error),
66    /// NUT04 Error
67    #[error(transparent)]
68    NUT04(#[from] crate::nuts::nut04::Error),
69    /// NUT05 Error
70    #[error(transparent)]
71    NUT05(#[from] crate::nuts::nut05::Error),
72    /// NUT23 Error
73    #[error(transparent)]
74    NUT23(#[from] crate::nuts::nut23::Error),
75    /// Hex error
76    #[error("Hex error")]
77    Hex(#[from] hex::Error),
78    /// Invalid hash
79    #[error("Invalid hash")]
80    InvalidHash,
81    /// Custom
82    #[error("`{0}`")]
83    Custom(String),
84}
85
86impl From<Infallible> for Error {
87    fn from(_: Infallible) -> Self {
88        unreachable!("Infallible cannot be constructed")
89    }
90}
91
92/// Payment identifier types
93#[derive(Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
94#[serde(tag = "type", content = "value")]
95pub enum PaymentIdentifier {
96    /// Label identifier
97    Label(String),
98    /// Offer ID identifier
99    OfferId(String),
100    /// Payment hash identifier
101    PaymentHash([u8; 32]),
102    /// Bolt12 payment hash
103    Bolt12PaymentHash([u8; 32]),
104    /// Payment id
105    PaymentId([u8; 32]),
106    /// Custom Payment ID
107    CustomId(String),
108    /// Quote ID
109    QuoteId(QuoteId),
110}
111
112impl PaymentIdentifier {
113    /// Create new [`PaymentIdentifier`]
114    pub fn new(kind: &str, identifier: &str) -> Result<Self, Error> {
115        match kind.to_lowercase().as_str() {
116            "label" => Ok(Self::Label(identifier.to_string())),
117            "offer_id" => Ok(Self::OfferId(identifier.to_string())),
118            "payment_hash" => Ok(Self::PaymentHash(
119                hex::decode(identifier)?
120                    .try_into()
121                    .map_err(|_| Error::InvalidHash)?,
122            )),
123            "bolt12_payment_hash" => Ok(Self::Bolt12PaymentHash(
124                hex::decode(identifier)?
125                    .try_into()
126                    .map_err(|_| Error::InvalidHash)?,
127            )),
128            "custom" => Ok(Self::CustomId(identifier.to_string())),
129            "payment_id" => Ok(Self::PaymentId(
130                hex::decode(identifier)?
131                    .try_into()
132                    .map_err(|_| Error::InvalidHash)?,
133            )),
134            "quote_id" => {
135                Ok(Self::QuoteId(identifier.parse().map_err(|_| {
136                    Error::Custom("Invalid QuoteId".to_string())
137                })?))
138            }
139            _ => Err(Error::UnsupportedPaymentOption),
140        }
141    }
142
143    /// Payment id kind
144    pub fn kind(&self) -> String {
145        match self {
146            Self::Label(_) => "label".to_string(),
147            Self::OfferId(_) => "offer_id".to_string(),
148            Self::PaymentHash(_) => "payment_hash".to_string(),
149            Self::Bolt12PaymentHash(_) => "bolt12_payment_hash".to_string(),
150            Self::PaymentId(_) => "payment_id".to_string(),
151            Self::CustomId(_) => "custom".to_string(),
152            Self::QuoteId(_) => "quote_id".to_string(),
153        }
154    }
155}
156
157impl std::fmt::Display for PaymentIdentifier {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        match self {
160            Self::Label(l) => write!(f, "{l}"),
161            Self::OfferId(o) => write!(f, "{o}"),
162            Self::PaymentHash(h) => write!(f, "{}", hex::encode(h)),
163            Self::Bolt12PaymentHash(h) => write!(f, "{}", hex::encode(h)),
164            Self::PaymentId(h) => write!(f, "{}", hex::encode(h)),
165            Self::CustomId(c) => write!(f, "{c}"),
166            Self::QuoteId(q) => write!(f, "{q}"),
167        }
168    }
169}
170
171impl std::fmt::Debug for PaymentIdentifier {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        match self {
174            PaymentIdentifier::PaymentHash(h) => write!(f, "PaymentHash({})", hex::encode(h)),
175            PaymentIdentifier::Bolt12PaymentHash(h) => {
176                write!(f, "Bolt12PaymentHash({})", hex::encode(h))
177            }
178            PaymentIdentifier::PaymentId(h) => write!(f, "PaymentId({})", hex::encode(h)),
179            PaymentIdentifier::Label(s) => write!(f, "Label({})", s),
180            PaymentIdentifier::OfferId(s) => write!(f, "OfferId({})", s),
181            PaymentIdentifier::CustomId(s) => write!(f, "CustomId({})", s),
182            PaymentIdentifier::QuoteId(q) => write!(f, "QuoteId({})", q),
183        }
184    }
185}
186
187/// Options for creating a BOLT11 incoming payment request
188#[derive(Debug, Clone, PartialEq, Eq, Hash)]
189pub struct Bolt11IncomingPaymentOptions {
190    /// Optional description for the payment request
191    pub description: Option<String>,
192    /// Amount for the payment request in sats
193    pub amount: Amount<CurrencyUnit>,
194    /// Optional expiry time as Unix timestamp in seconds
195    pub unix_expiry: Option<u64>,
196}
197
198impl Default for Bolt11IncomingPaymentOptions {
199    fn default() -> Self {
200        Self {
201            description: None,
202            amount: Amount::new(0, CurrencyUnit::Sat),
203            unix_expiry: None,
204        }
205    }
206}
207
208/// Options for creating a BOLT12 incoming payment request
209#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
210pub struct Bolt12IncomingPaymentOptions {
211    /// Optional description for the payment request
212    pub description: Option<String>,
213    /// Optional amount for the payment request in sats
214    pub amount: Option<Amount<CurrencyUnit>>,
215    /// Optional expiry time as Unix timestamp in seconds
216    pub unix_expiry: Option<u64>,
217}
218
219/// Options for creating a custom incoming payment request
220#[derive(Debug, Clone, PartialEq, Eq, Hash)]
221pub struct CustomIncomingPaymentOptions {
222    /// Payment method name (e.g., "paypal", "venmo")
223    pub method: String,
224    /// Optional description for the payment request
225    pub description: Option<String>,
226    /// Optional amount for the payment request
227    pub amount: Option<Amount<CurrencyUnit>>,
228    /// Optional expiry time as Unix timestamp in seconds
229    pub unix_expiry: Option<u64>,
230    /// Extra payment-method-specific fields as JSON string
231    ///
232    /// These fields are passed through to the payment processor for
233    /// method-specific validation (e.g., ehash share).
234    pub extra_json: Option<String>,
235    /// The mint's quote id for this mint quote. Generated before
236    /// `create_incoming_payment_request` is called, so backends whose rail is
237    /// keyed by the quote the wallet displays can register it up front and
238    /// use it as their stable correlation key — mirroring
239    /// [`OnchainIncomingPaymentOptions::quote_id`] and the melt-side
240    /// [`CustomOutgoingPaymentOptions::quote_id`].
241    pub quote_id: QuoteId,
242    /// NUT-20 locking pubkey of the quote, when the wallet supplied one.
243    ///
244    /// Lets backends enforce a locked-quotes-only policy at quote creation
245    /// for rails where the NUT-20 lock is the safety mechanism (the mint
246    /// itself only verifies signatures at mint time and does not require a
247    /// pubkey for custom methods).
248    pub pubkey: Option<PublicKey>,
249}
250
251/// Options for creating an onchain incoming payment request
252#[derive(Debug, Clone, PartialEq, Eq, Hash)]
253pub struct OnchainIncomingPaymentOptions {
254    /// Quote ID for the incoming payment
255    pub quote_id: QuoteId,
256}
257
258/// Options for incoming payments
259#[derive(Debug, Clone, PartialEq, Eq, Hash)]
260pub enum IncomingPaymentOptions {
261    /// BOLT11 payment request options
262    Bolt11(Bolt11IncomingPaymentOptions),
263    /// BOLT12 payment request options
264    Bolt12(Box<Bolt12IncomingPaymentOptions>),
265    /// Custom payment method options
266    Custom(Box<CustomIncomingPaymentOptions>),
267    /// Onchain payment request options
268    Onchain(OnchainIncomingPaymentOptions),
269}
270
271/// Options for BOLT11 outgoing payments
272#[derive(Debug, Clone, PartialEq, Eq, Hash)]
273pub struct Bolt11OutgoingPaymentOptions {
274    /// Bolt11
275    pub bolt11: Bolt11Invoice,
276    /// Maximum fee amount allowed for the payment
277    pub max_fee_amount: Option<Amount<CurrencyUnit>>,
278    /// Optional timeout in seconds
279    pub timeout_secs: Option<u64>,
280    /// Melt options
281    pub melt_options: Option<MeltOptions>,
282    /// The mint's quote id for this melt. Set in both `get_payment_quote`
283    /// and `make_payment` so backends can correlate the two calls. For
284    /// BOLT11 backends the payment_hash already provides correlation, so
285    /// this is informational; it is still required for protocol uniformity.
286    pub quote_id: QuoteId,
287}
288
289/// Options for BOLT12 outgoing payments
290#[derive(Debug, Clone, PartialEq, Eq, Hash)]
291pub struct Bolt12OutgoingPaymentOptions {
292    /// Offer
293    pub offer: Offer,
294    /// Maximum fee amount allowed for the payment
295    pub max_fee_amount: Option<Amount<CurrencyUnit>>,
296    /// Optional timeout in seconds
297    pub timeout_secs: Option<u64>,
298    /// Melt options
299    pub melt_options: Option<MeltOptions>,
300    /// The mint's quote id for this melt. See [`Bolt11OutgoingPaymentOptions::quote_id`].
301    pub quote_id: QuoteId,
302}
303
304/// Options for custom outgoing payments
305#[derive(Debug, Clone, PartialEq, Eq, Hash)]
306pub struct CustomOutgoingPaymentOptions {
307    /// Payment method name
308    pub method: String,
309    /// Payment request string (method-specific format)
310    pub request: String,
311    /// Optional amount the wallet would like to pay (from the melt quote request)
312    pub amount: Option<Amount<CurrencyUnit>>,
313    /// Maximum fee amount allowed for the payment
314    pub max_fee_amount: Option<Amount<CurrencyUnit>>,
315    /// Optional timeout in seconds
316    pub timeout_secs: Option<u64>,
317    /// Melt options
318    pub melt_options: Option<MeltOptions>,
319    /// Extra payment-method-specific fields as JSON string
320    ///
321    /// These fields are passed through to the payment processor for
322    /// method-specific validation.
323    pub extra_json: Option<String>,
324    /// The mint's quote id for this melt. Custom backends should use this
325    /// as the stable correlation key between `get_payment_quote` and
326    /// `make_payment` (and any later `check_outgoing_payment` polls) — it
327    /// is the only field guaranteed to be unique per melt without relying
328    /// on wallet-supplied uniqueness in `request`.
329    pub quote_id: QuoteId,
330}
331
332/// Options for onchain outgoing payments
333#[derive(Debug, Clone, PartialEq, Eq, Hash)]
334pub struct OnchainOutgoingPaymentOptions {
335    /// Bitcoin address to send to
336    pub address: String,
337    /// Payment amount
338    pub amount: Amount<CurrencyUnit>,
339    /// Maximum fee amount allowed for the payment
340    pub max_fee_amount: Option<Amount<CurrencyUnit>>,
341    /// Opaque stable identifier supplied by the mint.
342    ///
343    /// The mint generates this value and uses it to correlate the quote with
344    /// subsequent `make_payment` and `check_outgoing_payment` calls. Backends
345    /// MUST NOT synthesize or modify this value. Backends MUST persist it
346    /// (for example as the send intent id) and echo it verbatim in
347    /// [`PaymentQuoteResponse::request_lookup_id`] and
348    /// [`MakePaymentResponse::payment_lookup_id`] as
349    /// `PaymentIdentifier::QuoteId(..)`. The mint layer validates the echo
350    /// and will reject quotes whose backend response disagrees with the
351    /// supplied `quote_id` (see
352    /// [`Error::OnchainQuoteLookupIdMismatch`](crate::Error::OnchainQuoteLookupIdMismatch)).
353    pub quote_id: QuoteId,
354    /// Selected fee option index (mirrors the quote's chosen `fee_options[i].fee_index`)
355    pub fee_index: Option<u32>,
356    /// Opaque metadata as a JSON string for future extensions
357    pub metadata: Option<String>,
358}
359
360/// Options for outgoing payments
361#[derive(Debug, Clone, PartialEq, Eq, Hash)]
362pub enum OutgoingPaymentOptions {
363    /// BOLT11 payment options
364    Bolt11(Box<Bolt11OutgoingPaymentOptions>),
365    /// BOLT12 payment options
366    Bolt12(Box<Bolt12OutgoingPaymentOptions>),
367    /// Custom payment method options
368    Custom(Box<CustomOutgoingPaymentOptions>),
369    /// Onchain payment options
370    Onchain(Box<OnchainOutgoingPaymentOptions>),
371}
372
373impl OutgoingPaymentOptions {
374    /// Creates payment options from a melt quote
375    pub fn from_melt_quote_with_fee(
376        melt_quote: MeltQuote,
377    ) -> Result<OutgoingPaymentOptions, Error> {
378        let fee_reserve = melt_quote.fee_reserve();
379        let quote_id = melt_quote.id.clone();
380        match &melt_quote.request {
381            MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
382                Bolt11OutgoingPaymentOptions {
383                    max_fee_amount: Some(fee_reserve),
384                    timeout_secs: None,
385                    bolt11: bolt11.clone(),
386                    melt_options: melt_quote.options,
387                    quote_id,
388                },
389            ))),
390            MeltPaymentRequest::Bolt12 { offer } => {
391                let melt_options = match melt_quote.options {
392                    Some(MeltOptions::Mpp { mpp: _ }) => return Err(Error::UnsupportedUnit),
393                    Some(options) => Some(options),
394                    _ => None,
395                };
396
397                Ok(OutgoingPaymentOptions::Bolt12(Box::new(
398                    Bolt12OutgoingPaymentOptions {
399                        max_fee_amount: Some(fee_reserve),
400                        timeout_secs: None,
401                        offer: *offer.clone(),
402                        melt_options,
403                        quote_id,
404                    },
405                )))
406            }
407            MeltPaymentRequest::Custom { method, request } => Ok(OutgoingPaymentOptions::Custom(
408                Box::new(CustomOutgoingPaymentOptions {
409                    method: method.to_string(),
410                    request: request.to_string(),
411                    // Payment is already quoted; correlation is via quote_id.
412                    amount: None,
413                    max_fee_amount: Some(fee_reserve),
414                    timeout_secs: None,
415                    melt_options: melt_quote.options,
416                    extra_json: melt_quote
417                        .extra_json
418                        .as_ref()
419                        .map(serde_json::Value::to_string),
420                    quote_id,
421                }),
422            )),
423            MeltPaymentRequest::Onchain { address } => Ok(OutgoingPaymentOptions::Onchain(
424                Box::new(OnchainOutgoingPaymentOptions {
425                    address: address.clone(),
426                    amount: melt_quote.amount(),
427                    max_fee_amount: Some(fee_reserve),
428                    quote_id: melt_quote.id,
429                    fee_index: melt_quote.selected_fee_index,
430                    metadata: None,
431                }),
432            )),
433        }
434    }
435}
436
437/// Mint payment trait
438#[async_trait]
439pub trait MintPayment {
440    /// Payment backend error
441    type Err: Into<Error> + From<Error>;
442
443    /// Start the payment processor
444    /// Called when the mint starts up to initialize the payment processor
445    async fn start(&self) -> Result<(), Self::Err> {
446        // Default implementation - do nothing
447        Ok(())
448    }
449
450    /// Stop the payment processor
451    /// Called when the mint shuts down to gracefully stop the payment processor
452    async fn stop(&self) -> Result<(), Self::Err> {
453        // Default implementation - do nothing
454        Ok(())
455    }
456
457    /// Base Settings
458    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err>;
459
460    /// Create a new invoice
461    async fn create_incoming_payment_request(
462        &self,
463        options: IncomingPaymentOptions,
464    ) -> Result<CreateIncomingPaymentResponse, Self::Err>;
465
466    /// Get payment quote
467    /// Used to get fee and amount required for a payment request
468    async fn get_payment_quote(
469        &self,
470        unit: &CurrencyUnit,
471        options: OutgoingPaymentOptions,
472    ) -> Result<PaymentQuoteResponse, Self::Err>;
473
474    /// Pay request
475    async fn make_payment(
476        &self,
477        unit: &CurrencyUnit,
478        options: OutgoingPaymentOptions,
479    ) -> Result<MakePaymentResponse, Self::Err>;
480
481    /// Listen for invoices to be paid to the mint
482    /// Returns a stream of request_lookup_id once invoices are paid
483    async fn wait_payment_event(
484        &self,
485    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err>;
486
487    /// Is the payment event stream active
488    fn is_payment_event_stream_active(&self) -> bool;
489
490    /// Cancel the payment event stream
491    fn cancel_payment_event_stream(&self);
492
493    /// Check the status of an incoming payment
494    async fn check_incoming_payment_status(
495        &self,
496        payment_identifier: &PaymentIdentifier,
497    ) -> Result<Vec<WaitPaymentResponse>, Self::Err>;
498
499    /// Check the status of an outgoing payment
500    async fn check_outgoing_payment(
501        &self,
502        payment_identifier: &PaymentIdentifier,
503    ) -> Result<MakePaymentResponse, Self::Err>;
504}
505
506/// An event emitted which should be handled by the mint
507#[derive(Debug, Clone, Hash)]
508pub enum Event {
509    /// A payment has been received.
510    PaymentReceived(WaitPaymentResponse),
511    /// An outgoing payment has been confirmed.
512    PaymentSuccessful {
513        /// Quote ID linking to the melt quote
514        quote_id: QuoteId,
515        /// Payment response details
516        details: MakePaymentResponse,
517    },
518    /// An outgoing payment has permanently failed.
519    PaymentFailed {
520        /// Quote ID linking to the melt quote
521        quote_id: QuoteId,
522        /// Human-readable reason for the failure
523        reason: String,
524    },
525}
526
527/// Wait any invoice response
528#[derive(Debug, Clone, Hash)]
529pub struct WaitPaymentResponse {
530    /// Request look up id
531    /// Id that relates the quote and payment request
532    pub payment_identifier: PaymentIdentifier,
533    /// Payment amount (typed with unit for compile-time safety)
534    pub payment_amount: Amount<CurrencyUnit>,
535    /// Unique id of payment
536    // Payment hash
537    pub payment_id: String,
538}
539
540impl WaitPaymentResponse {
541    /// Get the currency unit
542    pub fn unit(&self) -> &CurrencyUnit {
543        self.payment_amount.unit()
544    }
545}
546
547/// Create incoming payment response
548#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
549pub struct CreateIncomingPaymentResponse {
550    /// Id that is used to look up the payment from the payment backend
551    pub request_lookup_id: PaymentIdentifier,
552    /// Payment request
553    pub request: String,
554    /// Unix Expiry of Invoice
555    pub expiry: Option<u64>,
556    /// Extra payment-method-specific fields
557    ///
558    /// These fields are flattened into the JSON representation, allowing
559    /// custom payment methods to include additional data without nesting.
560    #[serde(flatten, default)]
561    pub extra_json: Option<serde_json::Value>,
562}
563
564/// Payment response
565#[derive(Clone, Hash, PartialEq, Eq)]
566pub struct MakePaymentResponse {
567    /// Payment hash
568    ///
569    /// For onchain payments, this MUST be
570    /// `PaymentIdentifier::QuoteId(quote_id)` where `quote_id` is the value
571    /// supplied by the mint in
572    /// [`OnchainOutgoingPaymentOptions::quote_id`]. See that field for the
573    /// full echo contract.
574    pub payment_lookup_id: PaymentIdentifier,
575    /// Payment proof
576    pub payment_proof: Option<String>,
577    /// Status.
578    ///
579    /// When this response is returned by [`MintPayment::make_payment`],
580    /// [`MeltQuoteState::Failed`] and [`MeltQuoteState::Unpaid`] are treated as
581    /// authoritative terminal outcomes. Backends must return
582    /// [`MeltQuoteState::Pending`], [`MeltQuoteState::Unknown`], or an error if
583    /// they are uncertain whether payment dispatch can still settle.
584    pub status: MeltQuoteState,
585    /// Total amount spent, including fees. Only authoritative when `status`
586    /// is [`MeltQuoteState::Paid`]; otherwise backends return `0`.
587    pub total_spent: Amount<CurrencyUnit>,
588}
589
590impl fmt::Debug for MakePaymentResponse {
591    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
592        f.debug_struct("MakePaymentResponse")
593            .field("payment_lookup_id", &self.payment_lookup_id)
594            .field(
595                "payment_proof",
596                &self.payment_proof.as_ref().map(|_| "[REDACTED]"),
597            )
598            .field("status", &self.status)
599            .field("total_spent", &self.total_spent)
600            .finish()
601    }
602}
603
604impl MakePaymentResponse {
605    /// Get the currency unit
606    pub fn unit(&self) -> &CurrencyUnit {
607        self.total_spent.unit()
608    }
609}
610
611/// Payment quote response
612#[derive(Debug, Clone, Hash, PartialEq, Eq)]
613pub struct PaymentQuoteResponse {
614    /// Request look up id
615    ///
616    /// For onchain quotes, this MUST be
617    /// `Some(PaymentIdentifier::QuoteId(quote_id))` where `quote_id` is the
618    /// value supplied by the mint in
619    /// [`OnchainOutgoingPaymentOptions::quote_id`]. The mint validates this
620    /// echo and rejects mismatches — see
621    /// [`OnchainOutgoingPaymentOptions::quote_id`] for the full contract.
622    pub request_lookup_id: Option<PaymentIdentifier>,
623    /// Amount (typed with unit for compile-time safety)
624    pub amount: Amount<CurrencyUnit>,
625    /// Fee required for melt (typed with unit for compile-time safety)
626    pub fee: Amount<CurrencyUnit>,
627    /// Status
628    pub state: MeltQuoteState,
629    /// Extra payment-method-specific fields
630    pub extra_json: Option<serde_json::Value>,
631    /// Estimated confirmation target in blocks for onchain quotes.
632    ///
633    /// Onchain backends must return explicit `fee_options`; this field remains
634    /// a convenience mirror of the quoted or selected confirmation target.
635    pub estimated_blocks: Option<u32>,
636    /// Explicit onchain fee options the backend is willing to honor.
637    ///
638    /// For onchain melt quotes the mint enforces that `fee_options` is
639    /// non-empty.
640    ///
641    /// Backends assign stable `fee_index` values and must be able to honor the
642    /// selected value later in [`OnchainOutgoingPaymentOptions::fee_index`].
643    /// The mint validates, persists, and exposes these values unchanged.
644    /// Onchain backends must return `Some(vec)` here. Empty vectors produce
645    /// [`Error::OnchainFeeOptionsEmpty`](crate::Error::OnchainFeeOptionsEmpty),
646    /// and the quote is not persisted.
647    pub fee_options: Option<Vec<MeltQuoteOnchainFeeOption>>,
648}
649
650impl PaymentQuoteResponse {
651    /// Get the currency unit
652    pub fn unit(&self) -> &CurrencyUnit {
653        self.amount.unit()
654    }
655}
656
657/// BOLT11 settings
658#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
659pub struct Bolt11Settings {
660    /// Multi-part payment (MPP) supported
661    pub mpp: bool,
662    /// Amountless invoice support
663    pub amountless: bool,
664    /// Invoice description supported
665    pub invoice_description: bool,
666}
667
668/// BOLT12 settings
669#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
670pub struct Bolt12Settings {
671    /// Amountless offer support
672    pub amountless: bool,
673}
674
675/// Onchain settings
676#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
677pub struct OnchainSettings {
678    /// Number of confirmations required
679    pub confirmations: u32,
680    /// Minimum incoming onchain payment amount accepted by the backend
681    pub min_receive_amount_sat: u64,
682    /// Minimum outgoing onchain payment amount accepted by the backend
683    pub min_send_amount_sat: u64,
684}
685
686/// Payment processor settings response
687/// Mirrors the proto SettingsResponse structure
688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
689pub struct SettingsResponse {
690    /// Base unit of backend
691    pub unit: String,
692    /// BOLT11 settings (None if not supported)
693    pub bolt11: Option<Bolt11Settings>,
694    /// BOLT12 settings (None if not supported)
695    pub bolt12: Option<Bolt12Settings>,
696    /// Onchain settings (None if not supported)
697    pub onchain: Option<OnchainSettings>,
698    /// Custom payment methods settings (method name -> settings data)
699    #[serde(default)]
700    pub custom: std::collections::HashMap<String, String>,
701}
702
703impl From<SettingsResponse> for Value {
704    fn from(value: SettingsResponse) -> Self {
705        serde_json::to_value(value).unwrap_or(Value::Null)
706    }
707}
708
709impl TryFrom<Value> for SettingsResponse {
710    type Error = crate::error::Error;
711
712    fn try_from(value: Value) -> Result<Self, Self::Error> {
713        serde_json::from_value(value).map_err(|err| err.into())
714    }
715}
716
717/// Metrics wrapper for MintPayment implementations
718///
719/// This wrapper implements the Decorator pattern to collect metrics on all
720/// MintPayment trait methods. It wraps any existing MintPayment implementation
721/// and automatically records timing and operation metrics.
722#[derive(Debug, Clone)]
723#[cfg(feature = "prometheus")]
724pub struct MetricsMintPayment<T> {
725    inner: T,
726}
727#[cfg(feature = "prometheus")]
728impl<T> MetricsMintPayment<T>
729where
730    T: MintPayment,
731{
732    /// Create a new metrics wrapper around a MintPayment implementation
733    pub fn new(inner: T) -> Self {
734        Self { inner }
735    }
736
737    /// Get reference to the underlying implementation
738    pub fn inner(&self) -> &T {
739        &self.inner
740    }
741
742    /// Consume the wrapper and return the inner implementation
743    pub fn into_inner(self) -> T {
744        self.inner
745    }
746}
747
748#[async_trait]
749#[cfg(feature = "prometheus")]
750impl<T> MintPayment for MetricsMintPayment<T>
751where
752    T: MintPayment + Send + Sync,
753{
754    type Err = T::Err;
755
756    async fn start(&self) -> Result<(), Self::Err> {
757        let metrics = MintMetricGuard::new("start");
758
759        let result = self.inner.start().await;
760
761        metrics.record(result.is_ok());
762
763        result
764    }
765
766    async fn stop(&self) -> Result<(), Self::Err> {
767        let metrics = MintMetricGuard::new("stop");
768
769        let result = self.inner.stop().await;
770
771        metrics.record(result.is_ok());
772
773        result
774    }
775    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
776        let metrics = MintMetricGuard::new("get_settings");
777
778        let result = self.inner.get_settings().await;
779
780        metrics.record(result.is_ok());
781
782        result
783    }
784
785    async fn create_incoming_payment_request(
786        &self,
787        options: IncomingPaymentOptions,
788    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
789        let metrics = MintMetricGuard::new("create_incoming_payment_request");
790
791        let result = self.inner.create_incoming_payment_request(options).await;
792
793        metrics.record(result.is_ok());
794
795        result
796    }
797
798    async fn get_payment_quote(
799        &self,
800        unit: &CurrencyUnit,
801        options: OutgoingPaymentOptions,
802    ) -> Result<PaymentQuoteResponse, Self::Err> {
803        let metrics = MintMetricGuard::new("get_payment_quote");
804
805        let result = self.inner.get_payment_quote(unit, options).await;
806
807        metrics.record(result.is_ok());
808
809        result
810    }
811    async fn wait_payment_event(
812        &self,
813    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
814        let metrics = MintMetricGuard::new("wait_payment_event");
815
816        let result = self.inner.wait_payment_event().await;
817
818        let success = result.is_ok();
819
820        metrics.record(success);
821
822        result
823    }
824
825    async fn make_payment(
826        &self,
827        unit: &CurrencyUnit,
828        options: OutgoingPaymentOptions,
829    ) -> Result<MakePaymentResponse, Self::Err> {
830        let metrics = MintMetricGuard::new("make_payment");
831
832        let result = self.inner.make_payment(unit, options).await;
833
834        let success = result.is_ok();
835
836        metrics.record(success);
837
838        result
839    }
840
841    fn is_payment_event_stream_active(&self) -> bool {
842        self.inner.is_payment_event_stream_active()
843    }
844
845    fn cancel_payment_event_stream(&self) {
846        self.inner.cancel_payment_event_stream()
847    }
848
849    async fn check_incoming_payment_status(
850        &self,
851        payment_identifier: &PaymentIdentifier,
852    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
853        let metrics = MintMetricGuard::new("check_incoming_payment_status");
854
855        let result = self
856            .inner
857            .check_incoming_payment_status(payment_identifier)
858            .await;
859
860        metrics.record(result.is_ok());
861
862        result
863    }
864
865    async fn check_outgoing_payment(
866        &self,
867        payment_identifier: &PaymentIdentifier,
868    ) -> Result<MakePaymentResponse, Self::Err> {
869        let metrics = MintMetricGuard::new("check_outgoing_payment");
870
871        let result = self.inner.check_outgoing_payment(payment_identifier).await;
872
873        let success = result.is_ok();
874
875        metrics.record(success);
876
877        result
878    }
879}
880
881/// Type alias for Mint Payment trait
882pub type DynMintPayment = std::sync::Arc<dyn MintPayment<Err = Error> + Send + Sync>;
883
884#[cfg(test)]
885mod tests {
886    use std::str::FromStr;
887
888    use super::*;
889    use crate::QuoteId;
890
891    #[test]
892    fn make_payment_response_debug_redacts_payment_proof() {
893        let secret = "backend-payment-preimage-secret";
894        let response = MakePaymentResponse {
895            payment_lookup_id: PaymentIdentifier::CustomId("public-lookup-id".to_string()),
896            payment_proof: Some(secret.to_string()),
897            status: MeltQuoteState::Paid,
898            total_spent: Amount::new(10, CurrencyUnit::Sat),
899        };
900
901        let debug = format!("{response:?}");
902
903        assert!(debug.contains("public-lookup-id"));
904        assert!(debug.contains("[REDACTED]"));
905        assert!(!debug.contains(secret));
906    }
907
908    #[test]
909    fn test_payment_identifier_quote_id_roundtrip() {
910        let quote_id = QuoteId::new();
911        let identifier = PaymentIdentifier::QuoteId(quote_id.clone());
912
913        let kind = identifier.kind();
914        assert_eq!(kind, "quote_id");
915
916        let display = identifier.to_string();
917        assert_eq!(display, quote_id.to_string());
918
919        let debug = format!("{:?}", identifier);
920        assert_eq!(debug, format!("QuoteId({})", quote_id));
921
922        let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
923        assert_eq!(parsed, identifier);
924    }
925
926    #[test]
927    fn test_payment_identifier_quote_id_base64_roundtrip() {
928        let quote_id_str = "SGVsbG8gV29ybGQh"; // Valid Base64
929        let identifier = PaymentIdentifier::QuoteId(QuoteId::from_str(quote_id_str).unwrap());
930
931        let kind = identifier.kind();
932        assert_eq!(kind, "quote_id");
933
934        let display = identifier.to_string();
935        assert_eq!(display, quote_id_str);
936
937        let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
938        assert_eq!(parsed, identifier);
939    }
940
941    #[test]
942    fn test_payment_identifier_unsupported_kind() {
943        let result = PaymentIdentifier::new("unsupported_kind", "123");
944        assert!(matches!(result, Err(Error::UnsupportedPaymentOption)));
945    }
946
947    #[test]
948    fn test_payment_identifier_invalid_quote_id() {
949        // An invalid base64 and invalid UUID string (e.g. spaces and special characters)
950        let result = PaymentIdentifier::new("quote_id", "invalid!@#quote");
951        assert!(matches!(result, Err(Error::Custom(_))));
952    }
953
954    #[test]
955    fn test_payment_identifier_invalid_hash() {
956        // Invalid hex
957        let result_hex = PaymentIdentifier::new("payment_hash", "not_hex!");
958        assert!(matches!(result_hex, Err(Error::Hex(_))));
959
960        // Valid hex, but wrong length (e.g. 1 byte instead of 32)
961        let result_len = PaymentIdentifier::new("payment_hash", "00");
962        assert!(matches!(result_len, Err(Error::InvalidHash)));
963
964        // Invalid length for bolt12_payment_hash
965        let result_bolt12 = PaymentIdentifier::new("bolt12_payment_hash", "00");
966        assert!(matches!(result_bolt12, Err(Error::InvalidHash)));
967    }
968}
969
970#[test]
971fn test_payment_identifier_hash_variants_roundtrip() {
972    let dummy_hash = [1u8; 32];
973    let hex_encoded = hex::encode(dummy_hash);
974
975    // Test Bolt12PaymentHash
976    let bolt12_identifier = PaymentIdentifier::Bolt12PaymentHash(dummy_hash);
977
978    let kind = bolt12_identifier.kind();
979    assert_eq!(kind, "bolt12_payment_hash");
980
981    let display = bolt12_identifier.to_string();
982    assert_eq!(display, hex_encoded);
983
984    let debug = format!("{:?}", bolt12_identifier);
985    assert_eq!(debug, format!("Bolt12PaymentHash({})", hex_encoded));
986
987    let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
988    assert_eq!(parsed, bolt12_identifier);
989
990    // Test PaymentId
991    let dummy_hash_2 = [2u8; 32];
992    let hex_encoded_2 = hex::encode(dummy_hash_2);
993    let payment_id_identifier = PaymentIdentifier::PaymentId(dummy_hash_2);
994
995    let kind = payment_id_identifier.kind();
996    assert_eq!(kind, "payment_id");
997
998    let display = payment_id_identifier.to_string();
999    assert_eq!(display, hex_encoded_2);
1000
1001    let debug = format!("{:?}", payment_id_identifier);
1002    assert_eq!(debug, format!("PaymentId({})", hex_encoded_2));
1003
1004    let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
1005    assert_eq!(parsed, payment_id_identifier);
1006}