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};
9use futures::Stream;
10use lightning::offers::offer::Offer;
11use lightning_invoice::ParseOrSemanticError;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use thiserror::Error;
15
16use crate::mint::MeltPaymentRequest;
17use crate::nuts::{CurrencyUnit, MeltQuoteState};
18use crate::Amount;
19
20/// CDK Lightning Error
21#[derive(Debug, Error)]
22pub enum Error {
23    /// Invoice already paid
24    #[error("Invoice already paid")]
25    InvoiceAlreadyPaid,
26    /// Invoice pay pending
27    #[error("Invoice pay is pending")]
28    InvoicePaymentPending,
29    /// Unsupported unit
30    #[error("Unsupported unit")]
31    UnsupportedUnit,
32    /// Unsupported payment option
33    #[error("Unsupported payment option")]
34    UnsupportedPaymentOption,
35    /// Payment state is unknown
36    #[error("Payment state is unknown")]
37    UnknownPaymentState,
38    /// Amount mismatch
39    #[error("Amount is not what is expected")]
40    AmountMismatch,
41    /// Lightning Error
42    #[error(transparent)]
43    Lightning(Box<dyn std::error::Error + Send + Sync>),
44    /// Serde Error
45    #[error(transparent)]
46    Serde(#[from] serde_json::Error),
47    /// AnyHow Error
48    #[error(transparent)]
49    Anyhow(#[from] anyhow::Error),
50    /// Parse Error
51    #[error(transparent)]
52    Parse(#[from] ParseOrSemanticError),
53    /// Amount Error
54    #[error(transparent)]
55    Amount(#[from] crate::amount::Error),
56    /// NUT04 Error
57    #[error(transparent)]
58    NUT04(#[from] crate::nuts::nut04::Error),
59    /// NUT05 Error
60    #[error(transparent)]
61    NUT05(#[from] crate::nuts::nut05::Error),
62    /// NUT23 Error
63    #[error(transparent)]
64    NUT23(#[from] crate::nuts::nut23::Error),
65    /// Hex error
66    #[error("Hex error")]
67    Hex(#[from] hex::Error),
68    /// Invalid hash
69    #[error("Invalid hash")]
70    InvalidHash,
71    /// Custom
72    #[error("`{0}`")]
73    Custom(String),
74}
75
76impl From<Infallible> for Error {
77    fn from(_: Infallible) -> Self {
78        unreachable!("Infallible cannot be constructed")
79    }
80}
81
82/// Payment identifier types
83#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
84#[serde(tag = "type", content = "value")]
85pub enum PaymentIdentifier {
86    /// Label identifier
87    Label(String),
88    /// Offer ID identifier
89    OfferId(String),
90    /// Payment hash identifier
91    PaymentHash([u8; 32]),
92    /// Bolt12 payment hash
93    Bolt12PaymentHash([u8; 32]),
94    /// Payment id
95    PaymentId([u8; 32]),
96    /// Custom Payment ID
97    CustomId(String),
98}
99
100impl PaymentIdentifier {
101    /// Create new [`PaymentIdentifier`]
102    pub fn new(kind: &str, identifier: &str) -> Result<Self, Error> {
103        match kind.to_lowercase().as_str() {
104            "label" => Ok(Self::Label(identifier.to_string())),
105            "offer_id" => Ok(Self::OfferId(identifier.to_string())),
106            "payment_hash" => Ok(Self::PaymentHash(
107                hex::decode(identifier)?
108                    .try_into()
109                    .map_err(|_| Error::InvalidHash)?,
110            )),
111            "bolt12_payment_hash" => Ok(Self::Bolt12PaymentHash(
112                hex::decode(identifier)?
113                    .try_into()
114                    .map_err(|_| Error::InvalidHash)?,
115            )),
116            "custom" => Ok(Self::CustomId(identifier.to_string())),
117            "payment_id" => Ok(Self::PaymentId(
118                hex::decode(identifier)?
119                    .try_into()
120                    .map_err(|_| Error::InvalidHash)?,
121            )),
122            _ => Err(Error::UnsupportedPaymentOption),
123        }
124    }
125
126    /// Payment id kind
127    pub fn kind(&self) -> String {
128        match self {
129            Self::Label(_) => "label".to_string(),
130            Self::OfferId(_) => "offer_id".to_string(),
131            Self::PaymentHash(_) => "payment_hash".to_string(),
132            Self::Bolt12PaymentHash(_) => "bolt12_payment_hash".to_string(),
133            Self::PaymentId(_) => "payment_id".to_string(),
134            Self::CustomId(_) => "custom".to_string(),
135        }
136    }
137}
138
139impl std::fmt::Display for PaymentIdentifier {
140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141        match self {
142            Self::Label(l) => write!(f, "{l}"),
143            Self::OfferId(o) => write!(f, "{o}"),
144            Self::PaymentHash(h) => write!(f, "{}", hex::encode(h)),
145            Self::Bolt12PaymentHash(h) => write!(f, "{}", hex::encode(h)),
146            Self::PaymentId(h) => write!(f, "{}", hex::encode(h)),
147            Self::CustomId(c) => write!(f, "{c}"),
148        }
149    }
150}
151
152/// Options for creating a BOLT11 incoming payment request
153#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
154pub struct Bolt11IncomingPaymentOptions {
155    /// Optional description for the payment request
156    pub description: Option<String>,
157    /// Amount for the payment request in sats
158    pub amount: Amount,
159    /// Optional expiry time as Unix timestamp in seconds
160    pub unix_expiry: Option<u64>,
161}
162
163/// Options for creating a BOLT12 incoming payment request
164#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
165pub struct Bolt12IncomingPaymentOptions {
166    /// Optional description for the payment request
167    pub description: Option<String>,
168    /// Optional amount for the payment request in sats
169    pub amount: Option<Amount>,
170    /// Optional expiry time as Unix timestamp in seconds
171    pub unix_expiry: Option<u64>,
172}
173
174/// Options for creating an incoming payment request
175#[derive(Debug, Clone, PartialEq, Eq, Hash)]
176pub enum IncomingPaymentOptions {
177    /// BOLT11 payment request options
178    Bolt11(Bolt11IncomingPaymentOptions),
179    /// BOLT12 payment request options
180    Bolt12(Box<Bolt12IncomingPaymentOptions>),
181}
182
183/// Options for BOLT11 outgoing payments
184#[derive(Debug, Clone, PartialEq, Eq, Hash)]
185pub struct Bolt11OutgoingPaymentOptions {
186    /// Bolt11
187    pub bolt11: Bolt11Invoice,
188    /// Maximum fee amount allowed for the payment
189    pub max_fee_amount: Option<Amount>,
190    /// Optional timeout in seconds
191    pub timeout_secs: Option<u64>,
192    /// Melt options
193    pub melt_options: Option<MeltOptions>,
194}
195
196/// Options for BOLT12 outgoing payments
197#[derive(Debug, Clone, PartialEq, Eq, Hash)]
198pub struct Bolt12OutgoingPaymentOptions {
199    /// Offer
200    pub offer: Offer,
201    /// Maximum fee amount allowed for the payment
202    pub max_fee_amount: Option<Amount>,
203    /// Optional timeout in seconds
204    pub timeout_secs: Option<u64>,
205    /// Melt options
206    pub melt_options: Option<MeltOptions>,
207}
208
209/// Options for creating an outgoing payment
210#[derive(Debug, Clone, PartialEq, Eq, Hash)]
211pub enum OutgoingPaymentOptions {
212    /// BOLT11 payment options
213    Bolt11(Box<Bolt11OutgoingPaymentOptions>),
214    /// BOLT12 payment options
215    Bolt12(Box<Bolt12OutgoingPaymentOptions>),
216}
217
218impl TryFrom<crate::mint::MeltQuote> for OutgoingPaymentOptions {
219    type Error = Error;
220
221    fn try_from(melt_quote: crate::mint::MeltQuote) -> Result<Self, Self::Error> {
222        match melt_quote.request {
223            MeltPaymentRequest::Bolt11 { bolt11 } => Ok(OutgoingPaymentOptions::Bolt11(Box::new(
224                Bolt11OutgoingPaymentOptions {
225                    max_fee_amount: Some(melt_quote.fee_reserve),
226                    timeout_secs: None,
227                    bolt11,
228                    melt_options: melt_quote.options,
229                },
230            ))),
231            MeltPaymentRequest::Bolt12 { offer } => {
232                let melt_options = match melt_quote.options {
233                    None => None,
234                    Some(MeltOptions::Mpp { mpp: _ }) => return Err(Error::UnsupportedUnit),
235                    Some(options) => Some(options),
236                };
237
238                Ok(OutgoingPaymentOptions::Bolt12(Box::new(
239                    Bolt12OutgoingPaymentOptions {
240                        max_fee_amount: Some(melt_quote.fee_reserve),
241                        timeout_secs: None,
242                        offer: *offer,
243                        melt_options,
244                    },
245                )))
246            }
247        }
248    }
249}
250
251/// Mint payment trait
252#[async_trait]
253pub trait MintPayment {
254    /// Mint Lightning Error
255    type Err: Into<Error> + From<Error>;
256
257    /// Start the payment processor
258    /// Called when the mint starts up to initialize the payment processor
259    async fn start(&self) -> Result<(), Self::Err> {
260        // Default implementation - do nothing
261        Ok(())
262    }
263
264    /// Stop the payment processor
265    /// Called when the mint shuts down to gracefully stop the payment processor
266    async fn stop(&self) -> Result<(), Self::Err> {
267        // Default implementation - do nothing
268        Ok(())
269    }
270
271    /// Base Settings
272    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err>;
273
274    /// Create a new invoice
275    async fn create_incoming_payment_request(
276        &self,
277        unit: &CurrencyUnit,
278        options: IncomingPaymentOptions,
279    ) -> Result<CreateIncomingPaymentResponse, Self::Err>;
280
281    /// Get payment quote
282    /// Used to get fee and amount required for a payment request
283    async fn get_payment_quote(
284        &self,
285        unit: &CurrencyUnit,
286        options: OutgoingPaymentOptions,
287    ) -> Result<PaymentQuoteResponse, Self::Err>;
288
289    /// Pay request
290    async fn make_payment(
291        &self,
292        unit: &CurrencyUnit,
293        options: OutgoingPaymentOptions,
294    ) -> Result<MakePaymentResponse, Self::Err>;
295
296    /// Listen for invoices to be paid to the mint
297    /// Returns a stream of request_lookup_id once invoices are paid
298    async fn wait_any_incoming_payment(
299        &self,
300    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err>;
301
302    /// Is wait invoice active
303    fn is_wait_invoice_active(&self) -> bool;
304
305    /// Cancel wait invoice
306    fn cancel_wait_invoice(&self);
307
308    /// Check the status of an incoming payment
309    async fn check_incoming_payment_status(
310        &self,
311        payment_identifier: &PaymentIdentifier,
312    ) -> Result<Vec<WaitPaymentResponse>, Self::Err>;
313
314    /// Check the status of an outgoing payment
315    async fn check_outgoing_payment(
316        &self,
317        payment_identifier: &PaymentIdentifier,
318    ) -> Result<MakePaymentResponse, Self::Err>;
319}
320
321/// Wait any invoice response
322#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
323pub struct WaitPaymentResponse {
324    /// Request look up id
325    /// Id that relates the quote and payment request
326    pub payment_identifier: PaymentIdentifier,
327    /// Payment amount
328    pub payment_amount: Amount,
329    /// Unit
330    pub unit: CurrencyUnit,
331    /// Unique id of payment
332    // Payment hash
333    pub payment_id: String,
334}
335
336/// Create incoming payment response
337#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
338pub struct CreateIncomingPaymentResponse {
339    /// Id that is used to look up the payment from the ln backend
340    pub request_lookup_id: PaymentIdentifier,
341    /// Payment request
342    pub request: String,
343    /// Unix Expiry of Invoice
344    pub expiry: Option<u64>,
345}
346
347/// Payment response
348#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
349pub struct MakePaymentResponse {
350    /// Payment hash
351    pub payment_lookup_id: PaymentIdentifier,
352    /// Payment proof
353    pub payment_proof: Option<String>,
354    /// Status
355    pub status: MeltQuoteState,
356    /// Total Amount Spent
357    pub total_spent: Amount,
358    /// Unit of total spent
359    pub unit: CurrencyUnit,
360}
361
362/// Payment quote response
363#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
364pub struct PaymentQuoteResponse {
365    /// Request look up id
366    pub request_lookup_id: Option<PaymentIdentifier>,
367    /// Amount
368    pub amount: Amount,
369    /// Fee required for melt
370    pub fee: Amount,
371    /// Currency unit of `amount` and `fee`
372    pub unit: CurrencyUnit,
373    /// Status
374    pub state: MeltQuoteState,
375}
376
377/// Ln backend settings
378#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
379pub struct Bolt11Settings {
380    /// MPP supported
381    pub mpp: bool,
382    /// Base unit of backend
383    pub unit: CurrencyUnit,
384    /// Invoice Description supported
385    pub invoice_description: bool,
386    /// Paying amountless invoices supported
387    pub amountless: bool,
388    /// Bolt12 supported
389    pub bolt12: bool,
390}
391
392impl TryFrom<Bolt11Settings> for Value {
393    type Error = crate::error::Error;
394
395    fn try_from(value: Bolt11Settings) -> Result<Self, Self::Error> {
396        serde_json::to_value(value).map_err(|err| err.into())
397    }
398}
399
400impl TryFrom<Value> for Bolt11Settings {
401    type Error = crate::error::Error;
402
403    fn try_from(value: Value) -> Result<Self, Self::Error> {
404        serde_json::from_value(value).map_err(|err| err.into())
405    }
406}