Skip to main content

cdk_common/
payment.rs

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