Skip to main content

cdk_common/
payment.rs

1//! CDK Mint Lightning
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::METRICS;
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::nut_onchain::MeltQuoteOnchainFeeOption;
20use crate::nuts::{CurrencyUnit, MeltQuoteState};
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    /// Lightning Error
48    #[error(transparent)]
49    Lightning(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    /// Amount for the payment request
226    pub amount: 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}
235
236/// Options for creating an onchain incoming payment request
237#[derive(Debug, Clone, PartialEq, Eq, Hash)]
238pub struct OnchainIncomingPaymentOptions {
239    /// Quote ID for the incoming payment
240    pub quote_id: QuoteId,
241}
242
243/// Options for incoming payments
244#[derive(Debug, Clone, PartialEq, Eq, Hash)]
245pub enum IncomingPaymentOptions {
246    /// BOLT11 payment request options
247    Bolt11(Bolt11IncomingPaymentOptions),
248    /// BOLT12 payment request options
249    Bolt12(Box<Bolt12IncomingPaymentOptions>),
250    /// Custom payment method options
251    Custom(Box<CustomIncomingPaymentOptions>),
252    /// Onchain payment request options
253    Onchain(OnchainIncomingPaymentOptions),
254}
255
256/// Options for BOLT11 outgoing payments
257#[derive(Debug, Clone, PartialEq, Eq, Hash)]
258pub struct Bolt11OutgoingPaymentOptions {
259    /// Bolt11
260    pub bolt11: Bolt11Invoice,
261    /// Maximum fee amount allowed for the payment
262    pub max_fee_amount: Option<Amount<CurrencyUnit>>,
263    /// Optional timeout in seconds
264    pub timeout_secs: Option<u64>,
265    /// Melt options
266    pub melt_options: Option<MeltOptions>,
267    /// The mint's quote id for this melt. Set in both `get_payment_quote`
268    /// and `make_payment` so backends can correlate the two calls. For
269    /// BOLT11 backends the payment_hash already provides correlation, so
270    /// this is informational; it is still required for protocol uniformity.
271    pub quote_id: QuoteId,
272}
273
274/// Options for BOLT12 outgoing payments
275#[derive(Debug, Clone, PartialEq, Eq, Hash)]
276pub struct Bolt12OutgoingPaymentOptions {
277    /// Offer
278    pub offer: Offer,
279    /// Maximum fee amount allowed for the payment
280    pub max_fee_amount: Option<Amount<CurrencyUnit>>,
281    /// Optional timeout in seconds
282    pub timeout_secs: Option<u64>,
283    /// Melt options
284    pub melt_options: Option<MeltOptions>,
285    /// The mint's quote id for this melt. See [`Bolt11OutgoingPaymentOptions::quote_id`].
286    pub quote_id: QuoteId,
287}
288
289/// Options for custom outgoing payments
290#[derive(Debug, Clone, PartialEq, Eq, Hash)]
291pub struct CustomOutgoingPaymentOptions {
292    /// Payment method name
293    pub method: String,
294    /// Payment request string (method-specific format)
295    pub request: String,
296    /// Maximum fee amount allowed for the payment
297    pub max_fee_amount: Option<Amount<CurrencyUnit>>,
298    /// Optional timeout in seconds
299    pub timeout_secs: Option<u64>,
300    /// Melt options
301    pub melt_options: Option<MeltOptions>,
302    /// Extra payment-method-specific fields as JSON string
303    ///
304    /// These fields are passed through to the payment processor for
305    /// method-specific validation.
306    pub extra_json: Option<String>,
307    /// The mint's quote id for this melt. Custom backends should use this
308    /// as the stable correlation key between `get_payment_quote` and
309    /// `make_payment` (and any later `check_outgoing_payment` polls) — it
310    /// is the only field guaranteed to be unique per melt without relying
311    /// on wallet-supplied uniqueness in `request`.
312    pub quote_id: QuoteId,
313}
314
315/// Options for onchain outgoing payments
316#[derive(Debug, Clone, PartialEq, Eq, Hash)]
317pub struct OnchainOutgoingPaymentOptions {
318    /// Bitcoin address to send to
319    pub address: String,
320    /// Payment amount
321    pub amount: Amount<CurrencyUnit>,
322    /// Maximum fee amount allowed for the payment
323    pub max_fee_amount: Option<Amount<CurrencyUnit>>,
324    /// Opaque stable identifier supplied by the mint.
325    ///
326    /// The mint generates this value and uses it to correlate the quote with
327    /// subsequent `make_payment` and `check_outgoing_payment` calls. Backends
328    /// MUST NOT synthesize or modify this value. Backends MUST persist it
329    /// (for example as the send intent id) and echo it verbatim in
330    /// [`PaymentQuoteResponse::request_lookup_id`] and
331    /// [`MakePaymentResponse::payment_lookup_id`] as
332    /// `PaymentIdentifier::QuoteId(..)`. The mint layer validates the echo
333    /// and will reject quotes whose backend response disagrees with the
334    /// supplied `quote_id` (see
335    /// [`Error::OnchainQuoteLookupIdMismatch`](crate::Error::OnchainQuoteLookupIdMismatch)).
336    pub quote_id: QuoteId,
337    /// Selected fee option index (mirrors the quote's chosen `fee_options[i].fee_index`)
338    pub fee_index: Option<u32>,
339    /// Opaque metadata as a JSON string for future extensions
340    pub metadata: Option<String>,
341}
342
343/// Options for outgoing payments
344#[derive(Debug, Clone, PartialEq, Eq, Hash)]
345pub enum OutgoingPaymentOptions {
346    /// BOLT11 payment options
347    Bolt11(Box<Bolt11OutgoingPaymentOptions>),
348    /// BOLT12 payment options
349    Bolt12(Box<Bolt12OutgoingPaymentOptions>),
350    /// Custom payment method options
351    Custom(Box<CustomOutgoingPaymentOptions>),
352    /// Onchain payment options
353    Onchain(Box<OnchainOutgoingPaymentOptions>),
354}
355
356impl OutgoingPaymentOptions {
357    /// Creates payment options from a melt quote
358    pub fn from_melt_quote_with_fee(
359        melt_quote: MeltQuote,
360    ) -> Result<OutgoingPaymentOptions, Error> {
361        let fee_reserve = melt_quote.fee_reserve();
362        let quote_id = melt_quote.id.clone();
363        match &melt_quote.request {
364            MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
365                Bolt11OutgoingPaymentOptions {
366                    max_fee_amount: Some(fee_reserve),
367                    timeout_secs: None,
368                    bolt11: bolt11.clone(),
369                    melt_options: melt_quote.options,
370                    quote_id,
371                },
372            ))),
373            MeltPaymentRequest::Bolt12 { offer } => {
374                let melt_options = match melt_quote.options {
375                    Some(MeltOptions::Mpp { mpp: _ }) => return Err(Error::UnsupportedUnit),
376                    Some(options) => Some(options),
377                    _ => None,
378                };
379
380                Ok(OutgoingPaymentOptions::Bolt12(Box::new(
381                    Bolt12OutgoingPaymentOptions {
382                        max_fee_amount: Some(fee_reserve),
383                        timeout_secs: None,
384                        offer: *offer.clone(),
385                        melt_options,
386                        quote_id,
387                    },
388                )))
389            }
390            MeltPaymentRequest::Custom { method, request } => Ok(OutgoingPaymentOptions::Custom(
391                Box::new(CustomOutgoingPaymentOptions {
392                    method: method.to_string(),
393                    request: request.to_string(),
394                    max_fee_amount: Some(fee_reserve),
395                    timeout_secs: None,
396                    melt_options: melt_quote.options,
397                    extra_json: None,
398                    quote_id,
399                }),
400            )),
401            MeltPaymentRequest::Onchain { address } => Ok(OutgoingPaymentOptions::Onchain(
402                Box::new(OnchainOutgoingPaymentOptions {
403                    address: address.clone(),
404                    amount: melt_quote.amount(),
405                    max_fee_amount: Some(fee_reserve),
406                    quote_id: melt_quote.id,
407                    fee_index: melt_quote.selected_fee_index,
408                    metadata: None,
409                }),
410            )),
411        }
412    }
413}
414
415/// Mint payment trait
416#[async_trait]
417pub trait MintPayment {
418    /// Mint Lightning Error
419    type Err: Into<Error> + From<Error>;
420
421    /// Start the payment processor
422    /// Called when the mint starts up to initialize the payment processor
423    async fn start(&self) -> Result<(), Self::Err> {
424        // Default implementation - do nothing
425        Ok(())
426    }
427
428    /// Stop the payment processor
429    /// Called when the mint shuts down to gracefully stop the payment processor
430    async fn stop(&self) -> Result<(), Self::Err> {
431        // Default implementation - do nothing
432        Ok(())
433    }
434
435    /// Base Settings
436    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err>;
437
438    /// Create a new invoice
439    async fn create_incoming_payment_request(
440        &self,
441        options: IncomingPaymentOptions,
442    ) -> Result<CreateIncomingPaymentResponse, Self::Err>;
443
444    /// Get payment quote
445    /// Used to get fee and amount required for a payment request
446    async fn get_payment_quote(
447        &self,
448        unit: &CurrencyUnit,
449        options: OutgoingPaymentOptions,
450    ) -> Result<PaymentQuoteResponse, Self::Err>;
451
452    /// Pay request
453    async fn make_payment(
454        &self,
455        unit: &CurrencyUnit,
456        options: OutgoingPaymentOptions,
457    ) -> Result<MakePaymentResponse, Self::Err>;
458
459    /// Listen for invoices to be paid to the mint
460    /// Returns a stream of request_lookup_id once invoices are paid
461    async fn wait_payment_event(
462        &self,
463    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err>;
464
465    /// Is the payment event stream active
466    fn is_payment_event_stream_active(&self) -> bool;
467
468    /// Cancel the payment event stream
469    fn cancel_payment_event_stream(&self);
470
471    /// Check the status of an incoming payment
472    async fn check_incoming_payment_status(
473        &self,
474        payment_identifier: &PaymentIdentifier,
475    ) -> Result<Vec<WaitPaymentResponse>, Self::Err>;
476
477    /// Check the status of an outgoing payment
478    async fn check_outgoing_payment(
479        &self,
480        payment_identifier: &PaymentIdentifier,
481    ) -> Result<MakePaymentResponse, Self::Err>;
482}
483
484/// An event emitted which should be handled by the mint
485#[derive(Debug, Clone, Hash)]
486pub enum Event {
487    /// A payment has been received.
488    PaymentReceived(WaitPaymentResponse),
489    /// An outgoing payment has been confirmed.
490    PaymentSuccessful {
491        /// Quote ID linking to the melt quote
492        quote_id: QuoteId,
493        /// Payment response details
494        details: MakePaymentResponse,
495    },
496    /// An outgoing payment has permanently failed.
497    PaymentFailed {
498        /// Quote ID linking to the melt quote
499        quote_id: QuoteId,
500        /// Human-readable reason for the failure
501        reason: String,
502    },
503}
504
505/// Wait any invoice response
506#[derive(Debug, Clone, Hash)]
507pub struct WaitPaymentResponse {
508    /// Request look up id
509    /// Id that relates the quote and payment request
510    pub payment_identifier: PaymentIdentifier,
511    /// Payment amount (typed with unit for compile-time safety)
512    pub payment_amount: Amount<CurrencyUnit>,
513    /// Unique id of payment
514    // Payment hash
515    pub payment_id: String,
516}
517
518impl WaitPaymentResponse {
519    /// Get the currency unit
520    pub fn unit(&self) -> &CurrencyUnit {
521        self.payment_amount.unit()
522    }
523}
524
525/// Create incoming payment response
526#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
527pub struct CreateIncomingPaymentResponse {
528    /// Id that is used to look up the payment from the ln backend
529    pub request_lookup_id: PaymentIdentifier,
530    /// Payment request
531    pub request: String,
532    /// Unix Expiry of Invoice
533    pub expiry: Option<u64>,
534    /// Extra payment-method-specific fields
535    ///
536    /// These fields are flattened into the JSON representation, allowing
537    /// custom payment methods to include additional data without nesting.
538    #[serde(flatten, default)]
539    pub extra_json: Option<serde_json::Value>,
540}
541
542/// Payment response
543#[derive(Debug, Clone, Hash, PartialEq, Eq)]
544pub struct MakePaymentResponse {
545    /// Payment hash
546    ///
547    /// For onchain payments, this MUST be
548    /// `PaymentIdentifier::QuoteId(quote_id)` where `quote_id` is the value
549    /// supplied by the mint in
550    /// [`OnchainOutgoingPaymentOptions::quote_id`]. See that field for the
551    /// full echo contract.
552    pub payment_lookup_id: PaymentIdentifier,
553    /// Payment proof
554    pub payment_proof: Option<String>,
555    /// Status
556    pub status: MeltQuoteState,
557    /// Total amount spent, including fees. Only authoritative when `status`
558    /// is [`MeltQuoteState::Paid`]; otherwise backends return `0`.
559    pub total_spent: Amount<CurrencyUnit>,
560}
561
562impl MakePaymentResponse {
563    /// Get the currency unit
564    pub fn unit(&self) -> &CurrencyUnit {
565        self.total_spent.unit()
566    }
567}
568
569/// Payment quote response
570#[derive(Debug, Clone, Hash, PartialEq, Eq)]
571pub struct PaymentQuoteResponse {
572    /// Request look up id
573    ///
574    /// For onchain quotes, this MUST be
575    /// `Some(PaymentIdentifier::QuoteId(quote_id))` where `quote_id` is the
576    /// value supplied by the mint in
577    /// [`OnchainOutgoingPaymentOptions::quote_id`]. The mint validates this
578    /// echo and rejects mismatches — see
579    /// [`OnchainOutgoingPaymentOptions::quote_id`] for the full contract.
580    pub request_lookup_id: Option<PaymentIdentifier>,
581    /// Amount (typed with unit for compile-time safety)
582    pub amount: Amount<CurrencyUnit>,
583    /// Fee required for melt (typed with unit for compile-time safety)
584    pub fee: Amount<CurrencyUnit>,
585    /// Status
586    pub state: MeltQuoteState,
587    /// Extra payment-method-specific fields
588    pub extra_json: Option<serde_json::Value>,
589    /// Estimated confirmation target in blocks for onchain quotes.
590    ///
591    /// Onchain backends must return explicit `fee_options`; this field remains
592    /// a convenience mirror of the quoted or selected confirmation target.
593    pub estimated_blocks: Option<u32>,
594    /// Explicit onchain fee options the backend is willing to honor.
595    ///
596    /// For onchain melt quotes the mint enforces that `fee_options` is
597    /// non-empty.
598    ///
599    /// Backends assign stable `fee_index` values and must be able to honor the
600    /// selected value later in [`OnchainOutgoingPaymentOptions::fee_index`].
601    /// The mint validates, persists, and exposes these values unchanged.
602    /// Onchain backends must return `Some(vec)` here. Empty vectors produce
603    /// [`Error::OnchainFeeOptionsEmpty`](crate::Error::OnchainFeeOptionsEmpty),
604    /// and the quote is not persisted.
605    pub fee_options: Option<Vec<MeltQuoteOnchainFeeOption>>,
606}
607
608impl PaymentQuoteResponse {
609    /// Get the currency unit
610    pub fn unit(&self) -> &CurrencyUnit {
611        self.amount.unit()
612    }
613}
614
615/// BOLT11 settings
616#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
617pub struct Bolt11Settings {
618    /// Multi-part payment (MPP) supported
619    pub mpp: bool,
620    /// Amountless invoice support
621    pub amountless: bool,
622    /// Invoice description supported
623    pub invoice_description: bool,
624}
625
626/// BOLT12 settings
627#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
628pub struct Bolt12Settings {
629    /// Amountless offer support
630    pub amountless: bool,
631}
632
633/// Onchain settings
634#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Default)]
635pub struct OnchainSettings {
636    /// Number of confirmations required
637    pub confirmations: u32,
638    /// Minimum incoming onchain payment amount accepted by the backend
639    pub min_receive_amount_sat: u64,
640    /// Minimum outgoing onchain payment amount accepted by the backend
641    pub min_send_amount_sat: u64,
642}
643
644/// Payment processor settings response
645/// Mirrors the proto SettingsResponse structure
646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
647pub struct SettingsResponse {
648    /// Base unit of backend
649    pub unit: String,
650    /// BOLT11 settings (None if not supported)
651    pub bolt11: Option<Bolt11Settings>,
652    /// BOLT12 settings (None if not supported)
653    pub bolt12: Option<Bolt12Settings>,
654    /// Onchain settings (None if not supported)
655    pub onchain: Option<OnchainSettings>,
656    /// Custom payment methods settings (method name -> settings data)
657    #[serde(default)]
658    pub custom: std::collections::HashMap<String, String>,
659}
660
661impl From<SettingsResponse> for Value {
662    fn from(value: SettingsResponse) -> Self {
663        serde_json::to_value(value).unwrap_or(Value::Null)
664    }
665}
666
667impl TryFrom<Value> for SettingsResponse {
668    type Error = crate::error::Error;
669
670    fn try_from(value: Value) -> Result<Self, Self::Error> {
671        serde_json::from_value(value).map_err(|err| err.into())
672    }
673}
674
675/// Metrics wrapper for MintPayment implementations
676///
677/// This wrapper implements the Decorator pattern to collect metrics on all
678/// MintPayment trait methods. It wraps any existing MintPayment implementation
679/// and automatically records timing and operation metrics.
680#[derive(Debug, Clone)]
681#[cfg(feature = "prometheus")]
682pub struct MetricsMintPayment<T> {
683    inner: T,
684}
685#[cfg(feature = "prometheus")]
686impl<T> MetricsMintPayment<T>
687where
688    T: MintPayment,
689{
690    /// Create a new metrics wrapper around a MintPayment implementation
691    pub fn new(inner: T) -> Self {
692        Self { inner }
693    }
694
695    /// Get reference to the underlying implementation
696    pub fn inner(&self) -> &T {
697        &self.inner
698    }
699
700    /// Consume the wrapper and return the inner implementation
701    pub fn into_inner(self) -> T {
702        self.inner
703    }
704}
705
706#[async_trait]
707#[cfg(feature = "prometheus")]
708impl<T> MintPayment for MetricsMintPayment<T>
709where
710    T: MintPayment + Send + Sync,
711{
712    type Err = T::Err;
713
714    async fn start(&self) -> Result<(), Self::Err> {
715        let start = std::time::Instant::now();
716        METRICS.inc_in_flight_requests("start");
717
718        let result = self.inner.start().await;
719
720        let duration = start.elapsed().as_secs_f64();
721        METRICS.record_mint_operation_histogram("start", result.is_ok(), duration);
722        METRICS.dec_in_flight_requests("start");
723
724        result
725    }
726
727    async fn stop(&self) -> Result<(), Self::Err> {
728        let start = std::time::Instant::now();
729        METRICS.inc_in_flight_requests("stop");
730
731        let result = self.inner.stop().await;
732
733        let duration = start.elapsed().as_secs_f64();
734        METRICS.record_mint_operation_histogram("stop", result.is_ok(), duration);
735        METRICS.dec_in_flight_requests("stop");
736
737        result
738    }
739    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
740        let start = std::time::Instant::now();
741        METRICS.inc_in_flight_requests("get_settings");
742
743        let result = self.inner.get_settings().await;
744
745        let duration = start.elapsed().as_secs_f64();
746        METRICS.record_mint_operation_histogram("get_settings", result.is_ok(), duration);
747        METRICS.dec_in_flight_requests("get_settings");
748
749        result
750    }
751
752    async fn create_incoming_payment_request(
753        &self,
754        options: IncomingPaymentOptions,
755    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
756        let start = std::time::Instant::now();
757        METRICS.inc_in_flight_requests("create_incoming_payment_request");
758
759        let result = self.inner.create_incoming_payment_request(options).await;
760
761        let duration = start.elapsed().as_secs_f64();
762        METRICS.record_mint_operation_histogram(
763            "create_incoming_payment_request",
764            result.is_ok(),
765            duration,
766        );
767        METRICS.dec_in_flight_requests("create_incoming_payment_request");
768
769        result
770    }
771
772    async fn get_payment_quote(
773        &self,
774        unit: &CurrencyUnit,
775        options: OutgoingPaymentOptions,
776    ) -> Result<PaymentQuoteResponse, Self::Err> {
777        let start = std::time::Instant::now();
778        METRICS.inc_in_flight_requests("get_payment_quote");
779
780        let result = self.inner.get_payment_quote(unit, options).await;
781
782        let duration = start.elapsed().as_secs_f64();
783        let success = result.is_ok();
784
785        if let Ok(ref quote) = result {
786            let amount: f64 = quote.amount.value() as f64;
787            let fee: f64 = quote.fee.value() as f64;
788            METRICS.record_lightning_payment(amount, fee);
789        }
790
791        METRICS.record_mint_operation_histogram("get_payment_quote", success, duration);
792        METRICS.dec_in_flight_requests("get_payment_quote");
793
794        result
795    }
796    async fn wait_payment_event(
797        &self,
798    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
799        let start = std::time::Instant::now();
800        METRICS.inc_in_flight_requests("wait_payment_event");
801
802        let result = self.inner.wait_payment_event().await;
803
804        let duration = start.elapsed().as_secs_f64();
805        let success = result.is_ok();
806
807        METRICS.record_mint_operation_histogram("wait_payment_event", success, duration);
808        METRICS.dec_in_flight_requests("wait_payment_event");
809
810        result
811    }
812
813    async fn make_payment(
814        &self,
815        unit: &CurrencyUnit,
816        options: OutgoingPaymentOptions,
817    ) -> Result<MakePaymentResponse, Self::Err> {
818        let start = std::time::Instant::now();
819        METRICS.inc_in_flight_requests("make_payment");
820
821        let result = self.inner.make_payment(unit, options).await;
822
823        let duration = start.elapsed().as_secs_f64();
824        let success = result.is_ok();
825
826        METRICS.record_mint_operation_histogram("make_payment", success, duration);
827        METRICS.dec_in_flight_requests("make_payment");
828
829        result
830    }
831
832    fn is_payment_event_stream_active(&self) -> bool {
833        self.inner.is_payment_event_stream_active()
834    }
835
836    fn cancel_payment_event_stream(&self) {
837        self.inner.cancel_payment_event_stream()
838    }
839
840    async fn check_incoming_payment_status(
841        &self,
842        payment_identifier: &PaymentIdentifier,
843    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
844        let start = std::time::Instant::now();
845        METRICS.inc_in_flight_requests("check_incoming_payment_status");
846
847        let result = self
848            .inner
849            .check_incoming_payment_status(payment_identifier)
850            .await;
851
852        let duration = start.elapsed().as_secs_f64();
853        METRICS.record_mint_operation_histogram(
854            "check_incoming_payment_status",
855            result.is_ok(),
856            duration,
857        );
858        METRICS.dec_in_flight_requests("check_incoming_payment_status");
859
860        result
861    }
862
863    async fn check_outgoing_payment(
864        &self,
865        payment_identifier: &PaymentIdentifier,
866    ) -> Result<MakePaymentResponse, Self::Err> {
867        let start = std::time::Instant::now();
868        METRICS.inc_in_flight_requests("check_outgoing_payment");
869
870        let result = self.inner.check_outgoing_payment(payment_identifier).await;
871
872        let duration = start.elapsed().as_secs_f64();
873        let success = result.is_ok();
874
875        METRICS.record_mint_operation_histogram("check_outgoing_payment", success, duration);
876        METRICS.dec_in_flight_requests("check_outgoing_payment");
877
878        result
879    }
880}
881
882/// Type alias for Mint Payment trait
883pub type DynMintPayment = std::sync::Arc<dyn MintPayment<Err = Error> + Send + Sync>;
884
885#[cfg(test)]
886mod tests {
887    use std::str::FromStr;
888
889    use super::*;
890    use crate::QuoteId;
891
892    #[test]
893    fn test_payment_identifier_quote_id_roundtrip() {
894        let quote_id = QuoteId::new_uuid();
895        let identifier = PaymentIdentifier::QuoteId(quote_id.clone());
896
897        let kind = identifier.kind();
898        assert_eq!(kind, "quote_id");
899
900        let display = identifier.to_string();
901        assert_eq!(display, quote_id.to_string());
902
903        let debug = format!("{:?}", identifier);
904        assert_eq!(debug, format!("QuoteId({})", quote_id));
905
906        let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
907        assert_eq!(parsed, identifier);
908    }
909
910    #[test]
911    fn test_payment_identifier_quote_id_base64_roundtrip() {
912        let quote_id_str = "SGVsbG8gV29ybGQh"; // Valid Base64
913        let identifier = PaymentIdentifier::QuoteId(QuoteId::from_str(quote_id_str).unwrap());
914
915        let kind = identifier.kind();
916        assert_eq!(kind, "quote_id");
917
918        let display = identifier.to_string();
919        assert_eq!(display, quote_id_str);
920
921        let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
922        assert_eq!(parsed, identifier);
923    }
924
925    #[test]
926    fn test_payment_identifier_unsupported_kind() {
927        let result = PaymentIdentifier::new("unsupported_kind", "123");
928        assert!(matches!(result, Err(Error::UnsupportedPaymentOption)));
929    }
930
931    #[test]
932    fn test_payment_identifier_invalid_quote_id() {
933        // An invalid base64 and invalid UUID string (e.g. spaces and special characters)
934        let result = PaymentIdentifier::new("quote_id", "invalid!@#quote");
935        assert!(matches!(result, Err(Error::Custom(_))));
936    }
937
938    #[test]
939    fn test_payment_identifier_invalid_hash() {
940        // Invalid hex
941        let result_hex = PaymentIdentifier::new("payment_hash", "not_hex!");
942        assert!(matches!(result_hex, Err(Error::Hex(_))));
943
944        // Valid hex, but wrong length (e.g. 1 byte instead of 32)
945        let result_len = PaymentIdentifier::new("payment_hash", "00");
946        assert!(matches!(result_len, Err(Error::InvalidHash)));
947
948        // Invalid length for bolt12_payment_hash
949        let result_bolt12 = PaymentIdentifier::new("bolt12_payment_hash", "00");
950        assert!(matches!(result_bolt12, Err(Error::InvalidHash)));
951    }
952}
953
954#[test]
955fn test_payment_identifier_hash_variants_roundtrip() {
956    let dummy_hash = [1u8; 32];
957    let hex_encoded = hex::encode(dummy_hash);
958
959    // Test Bolt12PaymentHash
960    let bolt12_identifier = PaymentIdentifier::Bolt12PaymentHash(dummy_hash);
961
962    let kind = bolt12_identifier.kind();
963    assert_eq!(kind, "bolt12_payment_hash");
964
965    let display = bolt12_identifier.to_string();
966    assert_eq!(display, hex_encoded);
967
968    let debug = format!("{:?}", bolt12_identifier);
969    assert_eq!(debug, format!("Bolt12PaymentHash({})", hex_encoded));
970
971    let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
972    assert_eq!(parsed, bolt12_identifier);
973
974    // Test PaymentId
975    let dummy_hash_2 = [2u8; 32];
976    let hex_encoded_2 = hex::encode(dummy_hash_2);
977    let payment_id_identifier = PaymentIdentifier::PaymentId(dummy_hash_2);
978
979    let kind = payment_id_identifier.kind();
980    assert_eq!(kind, "payment_id");
981
982    let display = payment_id_identifier.to_string();
983    assert_eq!(display, hex_encoded_2);
984
985    let debug = format!("{:?}", payment_id_identifier);
986    assert_eq!(debug, format!("PaymentId({})", hex_encoded_2));
987
988    let parsed = PaymentIdentifier::new(&kind, &display).unwrap();
989    assert_eq!(parsed, payment_id_identifier);
990}