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    ///
501    /// The reported status must be conservative about finality. The mint
502    /// treats `Unpaid` and `Failed` as authoritative terminal outcomes and
503    /// may compensate the melt, returning the reserved proofs to the user.
504    /// A backend that cannot guarantee a not-paid payment will never
505    /// settle — for example because an orchestrator may be between attempts
506    /// of the same payment — MUST report `Pending` or `Unknown` instead.
507    async fn check_outgoing_payment(
508        &self,
509        payment_identifier: &PaymentIdentifier,
510    ) -> Result<MakePaymentResponse, Self::Err>;
511}
512
513/// An event emitted which should be handled by the mint
514#[derive(Debug, Clone, Hash)]
515pub enum Event {
516    /// A payment has been received.
517    PaymentReceived(WaitPaymentResponse),
518    /// An outgoing payment has been confirmed.
519    PaymentSuccessful {
520        /// Quote ID linking to the melt quote
521        quote_id: QuoteId,
522        /// Payment response details
523        details: MakePaymentResponse,
524    },
525    /// An outgoing payment has permanently failed.
526    PaymentFailed {
527        /// Quote ID linking to the melt quote
528        quote_id: QuoteId,
529        /// Human-readable reason for the failure
530        reason: String,
531    },
532}
533
534/// Wait any invoice response
535#[derive(Debug, Clone, Hash)]
536pub struct WaitPaymentResponse {
537    /// Request look up id
538    /// Id that relates the quote and payment request
539    pub payment_identifier: PaymentIdentifier,
540    /// Payment amount (typed with unit for compile-time safety)
541    pub payment_amount: Amount<CurrencyUnit>,
542    /// Unique id of payment
543    // Payment hash
544    pub payment_id: String,
545}
546
547impl WaitPaymentResponse {
548    /// Get the currency unit
549    pub fn unit(&self) -> &CurrencyUnit {
550        self.payment_amount.unit()
551    }
552}
553
554/// Create incoming payment response
555#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
556pub struct CreateIncomingPaymentResponse {
557    /// Id that is used to look up the payment from the payment backend
558    pub request_lookup_id: PaymentIdentifier,
559    /// Payment request
560    pub request: String,
561    /// Unix Expiry of Invoice
562    pub expiry: Option<u64>,
563    /// Extra payment-method-specific fields
564    ///
565    /// These fields are flattened into the JSON representation, allowing
566    /// custom payment methods to include additional data without nesting.
567    #[serde(flatten, default)]
568    pub extra_json: Option<serde_json::Value>,
569}
570
571/// Payment response
572#[derive(Clone, Hash, PartialEq, Eq)]
573pub struct MakePaymentResponse {
574    /// Payment hash
575    ///
576    /// For onchain payments, this MUST be
577    /// `PaymentIdentifier::QuoteId(quote_id)` where `quote_id` is the value
578    /// supplied by the mint in
579    /// [`OnchainOutgoingPaymentOptions::quote_id`]. See that field for the
580    /// full echo contract.
581    pub payment_lookup_id: PaymentIdentifier,
582    /// Payment proof
583    pub payment_proof: Option<String>,
584    /// Status.
585    ///
586    /// When this response is returned by [`MintPayment::make_payment`],
587    /// [`MeltQuoteState::Failed`] and [`MeltQuoteState::Unpaid`] are treated as
588    /// authoritative terminal outcomes. Backends must return
589    /// [`MeltQuoteState::Pending`], [`MeltQuoteState::Unknown`], or an error if
590    /// they are uncertain whether payment dispatch can still settle.
591    pub status: MeltQuoteState,
592    /// Total amount spent, including fees. Only authoritative when `status`
593    /// is [`MeltQuoteState::Paid`]; otherwise backends return `0`.
594    pub total_spent: Amount<CurrencyUnit>,
595}
596
597impl fmt::Debug for MakePaymentResponse {
598    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
599        f.debug_struct("MakePaymentResponse")
600            .field("payment_lookup_id", &self.payment_lookup_id)
601            .field(
602                "payment_proof",
603                &self.payment_proof.as_ref().map(|_| "[REDACTED]"),
604            )
605            .field("status", &self.status)
606            .field("total_spent", &self.total_spent)
607            .finish()
608    }
609}
610
611impl MakePaymentResponse {
612    /// Get the currency unit
613    pub fn unit(&self) -> &CurrencyUnit {
614        self.total_spent.unit()
615    }
616}
617
618/// Payment quote response
619#[derive(Debug, Clone, Hash, PartialEq, Eq)]
620pub struct PaymentQuoteResponse {
621    /// Request look up id
622    ///
623    /// For onchain quotes, this MUST be
624    /// `Some(PaymentIdentifier::QuoteId(quote_id))` where `quote_id` is the
625    /// value supplied by the mint in
626    /// [`OnchainOutgoingPaymentOptions::quote_id`]. The mint validates this
627    /// echo and rejects mismatches — see
628    /// [`OnchainOutgoingPaymentOptions::quote_id`] for the full contract.
629    pub request_lookup_id: Option<PaymentIdentifier>,
630    /// Amount (typed with unit for compile-time safety)
631    pub amount: Amount<CurrencyUnit>,
632    /// Fee required for melt (typed with unit for compile-time safety)
633    pub fee: Amount<CurrencyUnit>,
634    /// Status
635    pub state: MeltQuoteState,
636    /// Extra payment-method-specific fields
637    pub extra_json: Option<serde_json::Value>,
638    /// Estimated confirmation target in blocks for onchain quotes.
639    ///
640    /// Onchain backends must return explicit `fee_options`; this field remains
641    /// a convenience mirror of the quoted or selected confirmation target.
642    pub estimated_blocks: Option<u32>,
643    /// Explicit onchain fee options the backend is willing to honor.
644    ///
645    /// For onchain melt quotes the mint enforces that `fee_options` is
646    /// non-empty.
647    ///
648    /// Backends assign stable `fee_index` values and must be able to honor the
649    /// selected value later in [`OnchainOutgoingPaymentOptions::fee_index`].
650    /// The mint validates, persists, and exposes these values unchanged.
651    /// Onchain backends must return `Some(vec)` here. Empty vectors produce
652    /// [`Error::OnchainFeeOptionsEmpty`](crate::Error::OnchainFeeOptionsEmpty),
653    /// and the quote is not persisted.
654    pub fee_options: Option<Vec<MeltQuoteOnchainFeeOption>>,
655}
656
657impl PaymentQuoteResponse {
658    /// Get the currency unit
659    pub fn unit(&self) -> &CurrencyUnit {
660        self.amount.unit()
661    }
662}
663
664/// BOLT11 settings
665#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
666pub struct Bolt11Settings {
667    /// Multi-part payment (MPP) supported
668    pub mpp: bool,
669    /// Amountless invoice support
670    pub amountless: bool,
671    /// Invoice description supported
672    pub invoice_description: bool,
673}
674
675/// BOLT12 settings
676#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
677pub struct Bolt12Settings {
678    /// Amountless offer support
679    pub amountless: bool,
680    /// Offer description supported
681    pub invoice_description: bool,
682}
683
684/// Onchain settings
685#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
686pub struct OnchainSettings {
687    /// Number of confirmations required
688    pub confirmations: u32,
689    /// Minimum incoming onchain payment amount accepted by the backend
690    pub min_receive_amount_sat: u64,
691    /// Minimum outgoing onchain payment amount accepted by the backend
692    pub min_send_amount_sat: u64,
693}
694
695/// Payment processor settings response
696/// Mirrors the proto SettingsResponse structure
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698pub struct SettingsResponse {
699    /// Base unit of backend
700    pub unit: String,
701    /// BOLT11 settings (None if not supported)
702    pub bolt11: Option<Bolt11Settings>,
703    /// BOLT12 settings (None if not supported)
704    pub bolt12: Option<Bolt12Settings>,
705    /// Onchain settings (None if not supported)
706    pub onchain: Option<OnchainSettings>,
707    /// Custom payment methods settings (method name -> settings data)
708    #[serde(default)]
709    pub custom: std::collections::HashMap<String, String>,
710}
711
712impl From<SettingsResponse> for Value {
713    fn from(value: SettingsResponse) -> Self {
714        serde_json::to_value(value).unwrap_or(Value::Null)
715    }
716}
717
718impl TryFrom<Value> for SettingsResponse {
719    type Error = crate::error::Error;
720
721    fn try_from(value: Value) -> Result<Self, Self::Error> {
722        serde_json::from_value(value).map_err(|err| err.into())
723    }
724}
725
726/// Metrics wrapper for MintPayment implementations
727///
728/// This wrapper implements the Decorator pattern to collect metrics on all
729/// MintPayment trait methods. It wraps any existing MintPayment implementation
730/// and automatically records timing and operation metrics.
731#[derive(Debug, Clone)]
732#[cfg(feature = "prometheus")]
733pub struct MetricsMintPayment<T> {
734    inner: T,
735}
736#[cfg(feature = "prometheus")]
737impl<T> MetricsMintPayment<T>
738where
739    T: MintPayment,
740{
741    /// Create a new metrics wrapper around a MintPayment implementation
742    pub fn new(inner: T) -> Self {
743        Self { inner }
744    }
745
746    /// Get reference to the underlying implementation
747    pub fn inner(&self) -> &T {
748        &self.inner
749    }
750
751    /// Consume the wrapper and return the inner implementation
752    pub fn into_inner(self) -> T {
753        self.inner
754    }
755}
756
757#[async_trait]
758#[cfg(feature = "prometheus")]
759impl<T> MintPayment for MetricsMintPayment<T>
760where
761    T: MintPayment + Send + Sync,
762{
763    type Err = T::Err;
764
765    async fn start(&self) -> Result<(), Self::Err> {
766        let metrics = MintMetricGuard::new("start");
767
768        let result = self.inner.start().await;
769
770        metrics.record(result.is_ok());
771
772        result
773    }
774
775    async fn stop(&self) -> Result<(), Self::Err> {
776        let metrics = MintMetricGuard::new("stop");
777
778        let result = self.inner.stop().await;
779
780        metrics.record(result.is_ok());
781
782        result
783    }
784    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
785        let metrics = MintMetricGuard::new("get_settings");
786
787        let result = self.inner.get_settings().await;
788
789        metrics.record(result.is_ok());
790
791        result
792    }
793
794    async fn create_incoming_payment_request(
795        &self,
796        options: IncomingPaymentOptions,
797    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
798        let metrics = MintMetricGuard::new("create_incoming_payment_request");
799
800        let result = self.inner.create_incoming_payment_request(options).await;
801
802        metrics.record(result.is_ok());
803
804        result
805    }
806
807    async fn get_payment_quote(
808        &self,
809        unit: &CurrencyUnit,
810        options: OutgoingPaymentOptions,
811    ) -> Result<PaymentQuoteResponse, Self::Err> {
812        let metrics = MintMetricGuard::new("get_payment_quote");
813
814        let result = self.inner.get_payment_quote(unit, options).await;
815
816        metrics.record(result.is_ok());
817
818        result
819    }
820    async fn wait_payment_event(
821        &self,
822    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
823        let metrics = MintMetricGuard::new("wait_payment_event");
824
825        let result = self.inner.wait_payment_event().await;
826
827        let success = result.is_ok();
828
829        metrics.record(success);
830
831        result
832    }
833
834    async fn make_payment(
835        &self,
836        unit: &CurrencyUnit,
837        options: OutgoingPaymentOptions,
838    ) -> Result<MakePaymentResponse, Self::Err> {
839        let metrics = MintMetricGuard::new("make_payment");
840
841        let result = self.inner.make_payment(unit, options).await;
842
843        let success = result.is_ok();
844
845        metrics.record(success);
846
847        result
848    }
849
850    fn is_payment_event_stream_active(&self) -> bool {
851        self.inner.is_payment_event_stream_active()
852    }
853
854    fn cancel_payment_event_stream(&self) {
855        self.inner.cancel_payment_event_stream()
856    }
857
858    async fn check_incoming_payment_status(
859        &self,
860        payment_identifier: &PaymentIdentifier,
861    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
862        let metrics = MintMetricGuard::new("check_incoming_payment_status");
863
864        let result = self
865            .inner
866            .check_incoming_payment_status(payment_identifier)
867            .await;
868
869        metrics.record(result.is_ok());
870
871        result
872    }
873
874    async fn check_outgoing_payment(
875        &self,
876        payment_identifier: &PaymentIdentifier,
877    ) -> Result<MakePaymentResponse, Self::Err> {
878        let metrics = MintMetricGuard::new("check_outgoing_payment");
879
880        let result = self.inner.check_outgoing_payment(payment_identifier).await;
881
882        let success = result.is_ok();
883
884        metrics.record(success);
885
886        result
887    }
888}
889
890/// Type alias for Mint Payment trait
891pub type DynMintPayment = std::sync::Arc<dyn MintPayment<Err = Error> + Send + Sync>;
892
893#[cfg(test)]
894mod tests {
895    use std::str::FromStr;
896
897    use super::*;
898    use crate::QuoteId;
899
900    #[test]
901    fn make_payment_response_debug_redacts_payment_proof() {
902        let secret = "backend-payment-preimage-secret";
903        let response = MakePaymentResponse {
904            payment_lookup_id: PaymentIdentifier::CustomId("public-lookup-id".to_string()),
905            payment_proof: Some(secret.to_string()),
906            status: MeltQuoteState::Paid,
907            total_spent: Amount::new(10, CurrencyUnit::Sat),
908        };
909
910        let debug = format!("{response:?}");
911
912        assert!(debug.contains("public-lookup-id"));
913        assert!(debug.contains("[REDACTED]"));
914        assert!(!debug.contains(secret));
915    }
916
917    #[test]
918    fn test_payment_identifier_quote_id_roundtrip() {
919        let quote_id = QuoteId::new();
920        let identifier = PaymentIdentifier::QuoteId(quote_id.clone());
921
922        let kind = identifier.kind();
923        assert_eq!(kind, "quote_id");
924
925        let display = identifier.to_string();
926        assert_eq!(display, quote_id.to_string());
927
928        let debug = format!("{:?}", identifier);
929        assert_eq!(debug, format!("QuoteId({})", quote_id));
930
931        let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
932        assert_eq!(parsed, identifier);
933    }
934
935    #[test]
936    fn test_payment_identifier_quote_id_base64_roundtrip() {
937        let quote_id_str = "SGVsbG8gV29ybGQh"; // Valid Base64
938        let identifier = PaymentIdentifier::QuoteId(QuoteId::from_str(quote_id_str).unwrap());
939
940        let kind = identifier.kind();
941        assert_eq!(kind, "quote_id");
942
943        let display = identifier.to_string();
944        assert_eq!(display, quote_id_str);
945
946        let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
947        assert_eq!(parsed, identifier);
948    }
949
950    #[test]
951    fn test_payment_identifier_unsupported_kind() {
952        let result = PaymentIdentifier::new("unsupported_kind", "123");
953        assert!(matches!(result, Err(Error::UnsupportedPaymentOption)));
954    }
955
956    #[test]
957    fn test_payment_identifier_invalid_quote_id() {
958        // An invalid base64 and invalid UUID string (e.g. spaces and special characters)
959        let result = PaymentIdentifier::new("quote_id", "invalid!@#quote");
960        assert!(matches!(result, Err(Error::Custom(_))));
961    }
962
963    #[test]
964    fn test_payment_identifier_invalid_hash() {
965        // Invalid hex
966        let result_hex = PaymentIdentifier::new("payment_hash", "not_hex!");
967        assert!(matches!(result_hex, Err(Error::Hex(_))));
968
969        // Valid hex, but wrong length (e.g. 1 byte instead of 32)
970        let result_len = PaymentIdentifier::new("payment_hash", "00");
971        assert!(matches!(result_len, Err(Error::InvalidHash)));
972
973        // Invalid length for bolt12_payment_hash
974        let result_bolt12 = PaymentIdentifier::new("bolt12_payment_hash", "00");
975        assert!(matches!(result_bolt12, Err(Error::InvalidHash)));
976    }
977}
978
979#[test]
980fn test_payment_identifier_hash_variants_roundtrip() {
981    let dummy_hash = [1u8; 32];
982    let hex_encoded = hex::encode(dummy_hash);
983
984    // Test Bolt12PaymentHash
985    let bolt12_identifier = PaymentIdentifier::Bolt12PaymentHash(dummy_hash);
986
987    let kind = bolt12_identifier.kind();
988    assert_eq!(kind, "bolt12_payment_hash");
989
990    let display = bolt12_identifier.to_string();
991    assert_eq!(display, hex_encoded);
992
993    let debug = format!("{:?}", bolt12_identifier);
994    assert_eq!(debug, format!("Bolt12PaymentHash({})", hex_encoded));
995
996    let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
997    assert_eq!(parsed, bolt12_identifier);
998
999    // Test PaymentId
1000    let dummy_hash_2 = [2u8; 32];
1001    let hex_encoded_2 = hex::encode(dummy_hash_2);
1002    let payment_id_identifier = PaymentIdentifier::PaymentId(dummy_hash_2);
1003
1004    let kind = payment_id_identifier.kind();
1005    assert_eq!(kind, "payment_id");
1006
1007    let display = payment_id_identifier.to_string();
1008    assert_eq!(display, hex_encoded_2);
1009
1010    let debug = format!("{:?}", payment_id_identifier);
1011    assert_eq!(debug, format!("PaymentId({})", hex_encoded_2));
1012
1013    let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
1014    assert_eq!(parsed, payment_id_identifier);
1015}