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;
19use crate::nuts::{CurrencyUnit, MeltQuoteState};
20use crate::Amount;
21
22/// CDK Lightning Error
23#[derive(Debug, Error)]
24pub enum Error {
25    /// Invoice already paid
26    #[error("Invoice already paid")]
27    InvoiceAlreadyPaid,
28    /// Invoice pay pending
29    #[error("Invoice pay is pending")]
30    InvoicePaymentPending,
31    /// Unsupported unit
32    #[error("Unsupported unit")]
33    UnsupportedUnit,
34    /// Unsupported payment option
35    #[error("Unsupported payment option")]
36    UnsupportedPaymentOption,
37    /// Payment state is unknown
38    #[error("Payment state is unknown")]
39    UnknownPaymentState,
40    /// Amount mismatch
41    #[error("Amount is not what is expected")]
42    AmountMismatch,
43    /// Lightning Error
44    #[error(transparent)]
45    Lightning(Box<dyn std::error::Error + Send + Sync>),
46    /// Serde Error
47    #[error(transparent)]
48    Serde(#[from] serde_json::Error),
49    /// AnyHow Error
50    #[error(transparent)]
51    Anyhow(#[from] anyhow::Error),
52    /// Parse Error
53    #[error(transparent)]
54    Parse(#[from] ParseOrSemanticError),
55    /// Amount Error
56    #[error(transparent)]
57    Amount(#[from] crate::amount::Error),
58    /// NUT04 Error
59    #[error(transparent)]
60    NUT04(#[from] crate::nuts::nut04::Error),
61    /// NUT05 Error
62    #[error(transparent)]
63    NUT05(#[from] crate::nuts::nut05::Error),
64    /// NUT23 Error
65    #[error(transparent)]
66    NUT23(#[from] crate::nuts::nut23::Error),
67    /// Hex error
68    #[error("Hex error")]
69    Hex(#[from] hex::Error),
70    /// Invalid hash
71    #[error("Invalid hash")]
72    InvalidHash,
73    /// Custom
74    #[error("`{0}`")]
75    Custom(String),
76}
77
78impl From<Infallible> for Error {
79    fn from(_: Infallible) -> Self {
80        unreachable!("Infallible cannot be constructed")
81    }
82}
83
84/// Payment identifier types
85#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
86#[serde(tag = "type", content = "value")]
87pub enum PaymentIdentifier {
88    /// Label identifier
89    Label(String),
90    /// Offer ID identifier
91    OfferId(String),
92    /// Payment hash identifier
93    PaymentHash([u8; 32]),
94    /// Bolt12 payment hash
95    Bolt12PaymentHash([u8; 32]),
96    /// Payment id
97    PaymentId([u8; 32]),
98    /// Custom Payment ID
99    CustomId(String),
100}
101
102impl PaymentIdentifier {
103    /// Create new [`PaymentIdentifier`]
104    pub fn new(kind: &str, identifier: &str) -> Result<Self, Error> {
105        match kind.to_lowercase().as_str() {
106            "label" => Ok(Self::Label(identifier.to_string())),
107            "offer_id" => Ok(Self::OfferId(identifier.to_string())),
108            "payment_hash" => Ok(Self::PaymentHash(
109                hex::decode(identifier)?
110                    .try_into()
111                    .map_err(|_| Error::InvalidHash)?,
112            )),
113            "bolt12_payment_hash" => Ok(Self::Bolt12PaymentHash(
114                hex::decode(identifier)?
115                    .try_into()
116                    .map_err(|_| Error::InvalidHash)?,
117            )),
118            "custom" => Ok(Self::CustomId(identifier.to_string())),
119            "payment_id" => Ok(Self::PaymentId(
120                hex::decode(identifier)?
121                    .try_into()
122                    .map_err(|_| Error::InvalidHash)?,
123            )),
124            _ => Err(Error::UnsupportedPaymentOption),
125        }
126    }
127
128    /// Payment id kind
129    pub fn kind(&self) -> String {
130        match self {
131            Self::Label(_) => "label".to_string(),
132            Self::OfferId(_) => "offer_id".to_string(),
133            Self::PaymentHash(_) => "payment_hash".to_string(),
134            Self::Bolt12PaymentHash(_) => "bolt12_payment_hash".to_string(),
135            Self::PaymentId(_) => "payment_id".to_string(),
136            Self::CustomId(_) => "custom".to_string(),
137        }
138    }
139}
140
141impl std::fmt::Display for PaymentIdentifier {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        match self {
144            Self::Label(l) => write!(f, "{l}"),
145            Self::OfferId(o) => write!(f, "{o}"),
146            Self::PaymentHash(h) => write!(f, "{}", hex::encode(h)),
147            Self::Bolt12PaymentHash(h) => write!(f, "{}", hex::encode(h)),
148            Self::PaymentId(h) => write!(f, "{}", hex::encode(h)),
149            Self::CustomId(c) => write!(f, "{c}"),
150        }
151    }
152}
153
154/// Options for creating a BOLT11 incoming payment request
155#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
156pub struct Bolt11IncomingPaymentOptions {
157    /// Optional description for the payment request
158    pub description: Option<String>,
159    /// Amount for the payment request in sats
160    pub amount: Amount,
161    /// Optional expiry time as Unix timestamp in seconds
162    pub unix_expiry: Option<u64>,
163}
164
165/// Options for creating a BOLT12 incoming payment request
166#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
167pub struct Bolt12IncomingPaymentOptions {
168    /// Optional description for the payment request
169    pub description: Option<String>,
170    /// Optional amount for the payment request in sats
171    pub amount: Option<Amount>,
172    /// Optional expiry time as Unix timestamp in seconds
173    pub unix_expiry: Option<u64>,
174}
175
176/// Options for creating an incoming payment request
177#[derive(Debug, Clone, PartialEq, Eq, Hash)]
178pub enum IncomingPaymentOptions {
179    /// BOLT11 payment request options
180    Bolt11(Bolt11IncomingPaymentOptions),
181    /// BOLT12 payment request options
182    Bolt12(Box<Bolt12IncomingPaymentOptions>),
183}
184
185/// Options for BOLT11 outgoing payments
186#[derive(Debug, Clone, PartialEq, Eq, Hash)]
187pub struct Bolt11OutgoingPaymentOptions {
188    /// Bolt11
189    pub bolt11: Bolt11Invoice,
190    /// Maximum fee amount allowed for the payment
191    pub max_fee_amount: Option<Amount>,
192    /// Optional timeout in seconds
193    pub timeout_secs: Option<u64>,
194    /// Melt options
195    pub melt_options: Option<MeltOptions>,
196}
197
198/// Options for BOLT12 outgoing payments
199#[derive(Debug, Clone, PartialEq, Eq, Hash)]
200pub struct Bolt12OutgoingPaymentOptions {
201    /// Offer
202    pub offer: Offer,
203    /// Maximum fee amount allowed for the payment
204    pub max_fee_amount: Option<Amount>,
205    /// Optional timeout in seconds
206    pub timeout_secs: Option<u64>,
207    /// Melt options
208    pub melt_options: Option<MeltOptions>,
209}
210
211/// Options for creating an outgoing payment
212#[derive(Debug, Clone, PartialEq, Eq, Hash)]
213pub enum OutgoingPaymentOptions {
214    /// BOLT11 payment options
215    Bolt11(Box<Bolt11OutgoingPaymentOptions>),
216    /// BOLT12 payment options
217    Bolt12(Box<Bolt12OutgoingPaymentOptions>),
218}
219
220impl TryFrom<crate::mint::MeltQuote> for OutgoingPaymentOptions {
221    type Error = Error;
222
223    fn try_from(melt_quote: crate::mint::MeltQuote) -> Result<Self, Self::Error> {
224        match melt_quote.request {
225            MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
226                Bolt11OutgoingPaymentOptions {
227                    max_fee_amount: Some(melt_quote.fee_reserve),
228                    timeout_secs: None,
229                    bolt11,
230                    melt_options: melt_quote.options,
231                },
232            ))),
233            MeltPaymentRequest::Bolt12 { offer } => {
234                let melt_options = match melt_quote.options {
235                    None => None,
236                    Some(MeltOptions::Mpp { mpp: _ }) => return Err(Error::UnsupportedUnit),
237                    Some(options) => Some(options),
238                };
239
240                Ok(OutgoingPaymentOptions::Bolt12(Box::new(
241                    Bolt12OutgoingPaymentOptions {
242                        max_fee_amount: Some(melt_quote.fee_reserve),
243                        timeout_secs: None,
244                        offer: *offer,
245                        melt_options,
246                    },
247                )))
248            }
249        }
250    }
251}
252
253/// Mint payment trait
254#[async_trait]
255pub trait MintPayment {
256    /// Mint Lightning Error
257    type Err: Into<Error> + From<Error>;
258
259    /// Start the payment processor
260    /// Called when the mint starts up to initialize the payment processor
261    async fn start(&self) -> Result<(), Self::Err> {
262        // Default implementation - do nothing
263        Ok(())
264    }
265
266    /// Stop the payment processor
267    /// Called when the mint shuts down to gracefully stop the payment processor
268    async fn stop(&self) -> Result<(), Self::Err> {
269        // Default implementation - do nothing
270        Ok(())
271    }
272
273    /// Base Settings
274    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err>;
275
276    /// Create a new invoice
277    async fn create_incoming_payment_request(
278        &self,
279        unit: &CurrencyUnit,
280        options: IncomingPaymentOptions,
281    ) -> Result<CreateIncomingPaymentResponse, Self::Err>;
282
283    /// Get payment quote
284    /// Used to get fee and amount required for a payment request
285    async fn get_payment_quote(
286        &self,
287        unit: &CurrencyUnit,
288        options: OutgoingPaymentOptions,
289    ) -> Result<PaymentQuoteResponse, Self::Err>;
290
291    /// Pay request
292    async fn make_payment(
293        &self,
294        unit: &CurrencyUnit,
295        options: OutgoingPaymentOptions,
296    ) -> Result<MakePaymentResponse, Self::Err>;
297
298    /// Listen for invoices to be paid to the mint
299    /// Returns a stream of request_lookup_id once invoices are paid
300    async fn wait_payment_event(
301        &self,
302    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err>;
303
304    /// Is wait invoice active
305    fn is_wait_invoice_active(&self) -> bool;
306
307    /// Cancel wait invoice
308    fn cancel_wait_invoice(&self);
309
310    /// Check the status of an incoming payment
311    async fn check_incoming_payment_status(
312        &self,
313        payment_identifier: &PaymentIdentifier,
314    ) -> Result<Vec<WaitPaymentResponse>, Self::Err>;
315
316    /// Check the status of an outgoing payment
317    async fn check_outgoing_payment(
318        &self,
319        payment_identifier: &PaymentIdentifier,
320    ) -> Result<MakePaymentResponse, Self::Err>;
321}
322
323/// An event emitted which should be handled by the mint
324#[derive(Debug, Clone, Hash)]
325pub enum Event {
326    /// A payment has been received.
327    PaymentReceived(WaitPaymentResponse),
328}
329
330impl Default for Event {
331    fn default() -> Self {
332        // We use this as a sentinel value for no-op events
333        // The actual processing will filter these out
334        Event::PaymentReceived(WaitPaymentResponse {
335            payment_identifier: PaymentIdentifier::CustomId("default".to_string()),
336            payment_amount: Amount::from(0),
337            unit: CurrencyUnit::Msat,
338            payment_id: "default".to_string(),
339        })
340    }
341}
342
343/// Wait any invoice response
344#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
345pub struct WaitPaymentResponse {
346    /// Request look up id
347    /// Id that relates the quote and payment request
348    pub payment_identifier: PaymentIdentifier,
349    /// Payment amount
350    pub payment_amount: Amount,
351    /// Unit
352    pub unit: CurrencyUnit,
353    /// Unique id of payment
354    // Payment hash
355    pub payment_id: String,
356}
357
358/// Create incoming payment response
359#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
360pub struct CreateIncomingPaymentResponse {
361    /// Id that is used to look up the payment from the ln backend
362    pub request_lookup_id: PaymentIdentifier,
363    /// Payment request
364    pub request: String,
365    /// Unix Expiry of Invoice
366    pub expiry: Option<u64>,
367}
368
369/// Payment response
370#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
371pub struct MakePaymentResponse {
372    /// Payment hash
373    pub payment_lookup_id: PaymentIdentifier,
374    /// Payment proof
375    pub payment_proof: Option<String>,
376    /// Status
377    pub status: MeltQuoteState,
378    /// Total Amount Spent
379    pub total_spent: Amount,
380    /// Unit of total spent
381    pub unit: CurrencyUnit,
382}
383
384/// Payment quote response
385#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
386pub struct PaymentQuoteResponse {
387    /// Request look up id
388    pub request_lookup_id: Option<PaymentIdentifier>,
389    /// Amount
390    pub amount: Amount,
391    /// Fee required for melt
392    pub fee: Amount,
393    /// Currency unit of `amount` and `fee`
394    pub unit: CurrencyUnit,
395    /// Status
396    pub state: MeltQuoteState,
397}
398
399/// Ln backend settings
400#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
401pub struct Bolt11Settings {
402    /// MPP supported
403    pub mpp: bool,
404    /// Base unit of backend
405    pub unit: CurrencyUnit,
406    /// Invoice Description supported
407    pub invoice_description: bool,
408    /// Paying amountless invoices supported
409    pub amountless: bool,
410    /// Bolt12 supported
411    pub bolt12: bool,
412}
413
414impl TryFrom<Bolt11Settings> for Value {
415    type Error = crate::error::Error;
416
417    fn try_from(value: Bolt11Settings) -> Result<Self, Self::Error> {
418        serde_json::to_value(value).map_err(|err| err.into())
419    }
420}
421
422impl TryFrom<Value> for Bolt11Settings {
423    type Error = crate::error::Error;
424
425    fn try_from(value: Value) -> Result<Self, Self::Error> {
426        serde_json::from_value(value).map_err(|err| err.into())
427    }
428}
429
430/// Metrics wrapper for MintPayment implementations
431///
432/// This wrapper implements the Decorator pattern to collect metrics on all
433/// MintPayment trait methods. It wraps any existing MintPayment implementation
434/// and automatically records timing and operation metrics.
435#[derive(Clone)]
436#[cfg(feature = "prometheus")]
437pub struct MetricsMintPayment<T> {
438    inner: T,
439}
440#[cfg(feature = "prometheus")]
441impl<T> MetricsMintPayment<T>
442where
443    T: MintPayment,
444{
445    /// Create a new metrics wrapper around a MintPayment implementation
446    pub fn new(inner: T) -> Self {
447        Self { inner }
448    }
449
450    /// Get reference to the underlying implementation
451    pub fn inner(&self) -> &T {
452        &self.inner
453    }
454
455    /// Consume the wrapper and return the inner implementation
456    pub fn into_inner(self) -> T {
457        self.inner
458    }
459}
460
461#[async_trait]
462#[cfg(feature = "prometheus")]
463impl<T> MintPayment for MetricsMintPayment<T>
464where
465    T: MintPayment + Send + Sync,
466{
467    type Err = T::Err;
468
469    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
470        let start = std::time::Instant::now();
471        METRICS.inc_in_flight_requests("get_settings");
472
473        let result = self.inner.get_settings().await;
474
475        let duration = start.elapsed().as_secs_f64();
476        METRICS.record_mint_operation_histogram("get_settings", result.is_ok(), duration);
477        METRICS.dec_in_flight_requests("get_settings");
478
479        result
480    }
481
482    async fn create_incoming_payment_request(
483        &self,
484        unit: &CurrencyUnit,
485        options: IncomingPaymentOptions,
486    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
487        let start = std::time::Instant::now();
488        METRICS.inc_in_flight_requests("create_incoming_payment_request");
489
490        let result = self
491            .inner
492            .create_incoming_payment_request(unit, options)
493            .await;
494
495        let duration = start.elapsed().as_secs_f64();
496        METRICS.record_mint_operation_histogram(
497            "create_incoming_payment_request",
498            result.is_ok(),
499            duration,
500        );
501        METRICS.dec_in_flight_requests("create_incoming_payment_request");
502
503        result
504    }
505
506    async fn get_payment_quote(
507        &self,
508        unit: &CurrencyUnit,
509        options: OutgoingPaymentOptions,
510    ) -> Result<PaymentQuoteResponse, Self::Err> {
511        let start = std::time::Instant::now();
512        METRICS.inc_in_flight_requests("get_payment_quote");
513
514        let result = self.inner.get_payment_quote(unit, options).await;
515
516        let duration = start.elapsed().as_secs_f64();
517        let success = result.is_ok();
518
519        if let Ok(ref quote) = result {
520            let amount: f64 = u64::from(quote.amount) as f64;
521            let fee: f64 = u64::from(quote.fee) as f64;
522            METRICS.record_lightning_payment(amount, fee);
523        }
524
525        METRICS.record_mint_operation_histogram("get_payment_quote", success, duration);
526        METRICS.dec_in_flight_requests("get_payment_quote");
527
528        result
529    }
530    async fn wait_payment_event(
531        &self,
532    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
533        let start = std::time::Instant::now();
534        METRICS.inc_in_flight_requests("wait_payment_event");
535
536        let result = self.inner.wait_payment_event().await;
537
538        let duration = start.elapsed().as_secs_f64();
539        let success = result.is_ok();
540
541        METRICS.record_mint_operation_histogram("wait_payment_event", success, duration);
542        METRICS.dec_in_flight_requests("wait_payment_event");
543
544        result
545    }
546
547    async fn make_payment(
548        &self,
549        unit: &CurrencyUnit,
550        options: OutgoingPaymentOptions,
551    ) -> Result<MakePaymentResponse, Self::Err> {
552        let start = std::time::Instant::now();
553        METRICS.inc_in_flight_requests("make_payment");
554
555        let result = self.inner.make_payment(unit, options).await;
556
557        let duration = start.elapsed().as_secs_f64();
558        let success = result.is_ok();
559
560        METRICS.record_mint_operation_histogram("make_payment", success, duration);
561        METRICS.dec_in_flight_requests("make_payment");
562
563        result
564    }
565
566    fn is_wait_invoice_active(&self) -> bool {
567        self.inner.is_wait_invoice_active()
568    }
569
570    fn cancel_wait_invoice(&self) {
571        self.inner.cancel_wait_invoice()
572    }
573
574    async fn check_incoming_payment_status(
575        &self,
576        payment_identifier: &PaymentIdentifier,
577    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
578        let start = std::time::Instant::now();
579        METRICS.inc_in_flight_requests("check_incoming_payment_status");
580
581        let result = self
582            .inner
583            .check_incoming_payment_status(payment_identifier)
584            .await;
585
586        let duration = start.elapsed().as_secs_f64();
587        METRICS.record_mint_operation_histogram(
588            "check_incoming_payment_status",
589            result.is_ok(),
590            duration,
591        );
592        METRICS.dec_in_flight_requests("check_incoming_payment_status");
593
594        result
595    }
596
597    async fn check_outgoing_payment(
598        &self,
599        payment_identifier: &PaymentIdentifier,
600    ) -> Result<MakePaymentResponse, Self::Err> {
601        let start = std::time::Instant::now();
602        METRICS.inc_in_flight_requests("check_outgoing_payment");
603
604        let result = self.inner.check_outgoing_payment(payment_identifier).await;
605
606        let duration = start.elapsed().as_secs_f64();
607        let success = result.is_ok();
608
609        METRICS.record_mint_operation_histogram("check_outgoing_payment", success, duration);
610        METRICS.dec_in_flight_requests("check_outgoing_payment");
611
612        result
613    }
614}
615
616/// Type alias for Mint Payment trait
617pub type DynMintPayment = std::sync::Arc<dyn MintPayment<Err = Error> + Send + Sync>;