cdk_cln/
lib.rs

1//! CDK lightning backend for CLN
2
3#![doc = include_str!("../README.md")]
4#![warn(missing_docs)]
5#![warn(rustdoc::bare_urls)]
6
7use std::cmp::max;
8use std::path::PathBuf;
9use std::pin::Pin;
10use std::str::FromStr;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13
14use async_trait::async_trait;
15use cdk_common::amount::{to_unit, Amount};
16use cdk_common::common::FeeReserve;
17use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
18use cdk_common::payment::{
19    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
20    PaymentQuoteResponse,
21};
22use cdk_common::util::{hex, unix_time};
23use cdk_common::{mint, Bolt11Invoice};
24use cln_rpc::model::requests::{
25    InvoiceRequest, ListinvoicesRequest, ListpaysRequest, PayRequest, WaitanyinvoiceRequest,
26};
27use cln_rpc::model::responses::{
28    ListinvoicesInvoices, ListinvoicesInvoicesStatus, ListpaysPaysStatus, PayStatus,
29    WaitanyinvoiceStatus,
30};
31use cln_rpc::primitives::{Amount as CLN_Amount, AmountOrAny};
32use error::Error;
33use futures::{Stream, StreamExt};
34use serde_json::Value;
35use tokio::sync::Mutex;
36use tokio_util::sync::CancellationToken;
37use uuid::Uuid;
38
39pub mod error;
40
41/// CLN mint backend
42#[derive(Clone)]
43pub struct Cln {
44    rpc_socket: PathBuf,
45    cln_client: Arc<Mutex<cln_rpc::ClnRpc>>,
46    fee_reserve: FeeReserve,
47    wait_invoice_cancel_token: CancellationToken,
48    wait_invoice_is_active: Arc<AtomicBool>,
49}
50
51impl Cln {
52    /// Create new [`Cln`]
53    pub async fn new(rpc_socket: PathBuf, fee_reserve: FeeReserve) -> Result<Self, Error> {
54        let cln_client = cln_rpc::ClnRpc::new(&rpc_socket).await?;
55
56        Ok(Self {
57            rpc_socket,
58            cln_client: Arc::new(Mutex::new(cln_client)),
59            fee_reserve,
60            wait_invoice_cancel_token: CancellationToken::new(),
61            wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
62        })
63    }
64}
65
66#[async_trait]
67impl MintPayment for Cln {
68    type Err = payment::Error;
69
70    async fn get_settings(&self) -> Result<Value, Self::Err> {
71        Ok(serde_json::to_value(Bolt11Settings {
72            mpp: true,
73            unit: CurrencyUnit::Msat,
74            invoice_description: true,
75            amountless: true,
76        })?)
77    }
78
79    /// Is wait invoice active
80    fn is_wait_invoice_active(&self) -> bool {
81        self.wait_invoice_is_active.load(Ordering::SeqCst)
82    }
83
84    /// Cancel wait invoice
85    fn cancel_wait_invoice(&self) {
86        self.wait_invoice_cancel_token.cancel()
87    }
88
89    async fn wait_any_incoming_payment(
90        &self,
91    ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
92        let last_pay_index = self.get_last_pay_index().await?;
93        let cln_client = cln_rpc::ClnRpc::new(&self.rpc_socket).await?;
94
95        let stream = futures::stream::unfold(
96            (
97                cln_client,
98                last_pay_index,
99                self.wait_invoice_cancel_token.clone(),
100                Arc::clone(&self.wait_invoice_is_active),
101            ),
102            |(mut cln_client, mut last_pay_idx, cancel_token, is_active)| async move {
103                // Set the stream as active
104                is_active.store(true, Ordering::SeqCst);
105
106                loop {
107                    let request = WaitanyinvoiceRequest {
108                        timeout: None,
109                        lastpay_index: last_pay_idx,
110                    };
111                    tokio::select! {
112                        _ = cancel_token.cancelled() => {
113                            // Set the stream as inactive
114                            is_active.store(false, Ordering::SeqCst);
115                            // End the stream
116                            return None;
117                        }
118                        result = cln_client.call_typed(&request) => {
119                            match result {
120                                Ok(invoice) => {
121
122                            // Check the status of the invoice
123                            // We only want to yield invoices that have been paid
124                            match invoice.status {
125                                WaitanyinvoiceStatus::PAID => (),
126                                WaitanyinvoiceStatus::EXPIRED => continue,
127                            }
128
129                            last_pay_idx = invoice.pay_index;
130
131                            let payment_hash = invoice.payment_hash.to_string();
132
133                            let request_look_up = match invoice.bolt12 {
134                                // If it is a bolt12 payment we need to get the offer_id as this is what we use as the request look up.
135                                // Since this is not returned in the wait any response,
136                                // we need to do a second query for it.
137                                Some(_) => {
138                                    match fetch_invoice_by_payment_hash(
139                                        &mut cln_client,
140                                        &payment_hash,
141                                    )
142                                    .await
143                                    {
144                                        Ok(Some(invoice)) => {
145                                            if let Some(local_offer_id) = invoice.local_offer_id {
146                                                local_offer_id.to_string()
147                                            } else {
148                                                continue;
149                                            }
150                                        }
151                                        Ok(None) => continue,
152                                        Err(e) => {
153                                            tracing::warn!(
154                                                "Error fetching invoice by payment hash: {e}"
155                                            );
156                                            continue;
157                                        }
158                                    }
159                                }
160                                None => payment_hash,
161                            };
162
163                            return Some((request_look_up, (cln_client, last_pay_idx, cancel_token, is_active)));
164                                }
165                                Err(e) => {
166                                    tracing::warn!("Error fetching invoice: {e}");
167                                    is_active.store(false, Ordering::SeqCst);
168                                    return None;
169                                }
170                            }
171                        }
172                    }
173                }
174            },
175        )
176        .boxed();
177
178        Ok(stream)
179    }
180
181    async fn get_payment_quote(
182        &self,
183        request: &str,
184        unit: &CurrencyUnit,
185        options: Option<MeltOptions>,
186    ) -> Result<PaymentQuoteResponse, Self::Err> {
187        let bolt11 = Bolt11Invoice::from_str(request)?;
188
189        let amount_msat = match options {
190            Some(amount) => amount.amount_msat(),
191            None => bolt11
192                .amount_milli_satoshis()
193                .ok_or(Error::UnknownInvoiceAmount)?
194                .into(),
195        };
196
197        let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
198
199        let relative_fee_reserve =
200            (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
201
202        let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
203
204        let fee = max(relative_fee_reserve, absolute_fee_reserve);
205
206        Ok(PaymentQuoteResponse {
207            request_lookup_id: bolt11.payment_hash().to_string(),
208            amount,
209            fee: fee.into(),
210            state: MeltQuoteState::Unpaid,
211        })
212    }
213
214    async fn make_payment(
215        &self,
216        melt_quote: mint::MeltQuote,
217        partial_amount: Option<Amount>,
218        max_fee: Option<Amount>,
219    ) -> Result<MakePaymentResponse, Self::Err> {
220        let bolt11 = Bolt11Invoice::from_str(&melt_quote.request)?;
221        let pay_state = self
222            .check_outgoing_payment(&bolt11.payment_hash().to_string())
223            .await?;
224
225        match pay_state.status {
226            MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
227            MeltQuoteState::Paid => {
228                tracing::debug!("Melt attempted on invoice already paid");
229                return Err(Self::Err::InvoiceAlreadyPaid);
230            }
231            MeltQuoteState::Pending => {
232                tracing::debug!("Melt attempted on invoice already pending");
233                return Err(Self::Err::InvoicePaymentPending);
234            }
235        }
236
237        let amount_msat = partial_amount
238            .is_none()
239            .then(|| {
240                melt_quote
241                    .msat_to_pay
242                    .map(|a| CLN_Amount::from_msat(a.into()))
243            })
244            .flatten();
245
246        let mut cln_client = self.cln_client.lock().await;
247        let cln_response = cln_client
248            .call_typed(&PayRequest {
249                bolt11: melt_quote.request.to_string(),
250                amount_msat,
251                label: None,
252                riskfactor: None,
253                maxfeepercent: None,
254                retry_for: None,
255                maxdelay: None,
256                exemptfee: None,
257                localinvreqid: None,
258                exclude: None,
259                maxfee: max_fee
260                    .map(|a| {
261                        let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
262                        Ok::<CLN_Amount, Self::Err>(CLN_Amount::from_msat(msat.into()))
263                    })
264                    .transpose()?,
265                description: None,
266                partial_msat: partial_amount
267                    .map(|a| {
268                        let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
269
270                        Ok::<cln_rpc::primitives::Amount, Self::Err>(CLN_Amount::from_msat(
271                            msat.into(),
272                        ))
273                    })
274                    .transpose()?,
275            })
276            .await;
277
278        let response = match cln_response {
279            Ok(pay_response) => {
280                let status = match pay_response.status {
281                    PayStatus::COMPLETE => MeltQuoteState::Paid,
282                    PayStatus::PENDING => MeltQuoteState::Pending,
283                    PayStatus::FAILED => MeltQuoteState::Failed,
284                };
285
286                MakePaymentResponse {
287                    payment_proof: Some(hex::encode(pay_response.payment_preimage.to_vec())),
288                    payment_lookup_id: pay_response.payment_hash.to_string(),
289                    status,
290                    total_spent: to_unit(
291                        pay_response.amount_sent_msat.msat(),
292                        &CurrencyUnit::Msat,
293                        &melt_quote.unit,
294                    )?,
295                    unit: melt_quote.unit,
296                }
297            }
298            Err(err) => {
299                tracing::error!("Could not pay invoice: {}", err);
300                return Err(Error::ClnRpc(err).into());
301            }
302        };
303
304        Ok(response)
305    }
306
307    async fn create_incoming_payment_request(
308        &self,
309        amount: Amount,
310        unit: &CurrencyUnit,
311        description: String,
312        unix_expiry: Option<u64>,
313    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
314        let time_now = unix_time();
315
316        let mut cln_client = self.cln_client.lock().await;
317
318        let label = Uuid::new_v4().to_string();
319
320        let amount = to_unit(amount, unit, &CurrencyUnit::Msat)?;
321        let amount_msat = AmountOrAny::Amount(CLN_Amount::from_msat(amount.into()));
322
323        let invoice_response = cln_client
324            .call_typed(&InvoiceRequest {
325                amount_msat,
326                description,
327                label: label.clone(),
328                expiry: unix_expiry.map(|t| t - time_now),
329                fallbacks: None,
330                preimage: None,
331                cltv: None,
332                deschashonly: None,
333                exposeprivatechannels: None,
334            })
335            .await
336            .map_err(Error::from)?;
337
338        let request = Bolt11Invoice::from_str(&invoice_response.bolt11)?;
339        let expiry = request.expires_at().map(|t| t.as_secs());
340        let payment_hash = request.payment_hash();
341
342        Ok(CreateIncomingPaymentResponse {
343            request_lookup_id: payment_hash.to_string(),
344            request: request.to_string(),
345            expiry,
346        })
347    }
348
349    async fn check_incoming_payment_status(
350        &self,
351        payment_hash: &str,
352    ) -> Result<MintQuoteState, Self::Err> {
353        let mut cln_client = self.cln_client.lock().await;
354
355        let listinvoices_response = cln_client
356            .call_typed(&ListinvoicesRequest {
357                payment_hash: Some(payment_hash.to_string()),
358                label: None,
359                invstring: None,
360                offer_id: None,
361                index: None,
362                limit: None,
363                start: None,
364            })
365            .await
366            .map_err(Error::from)?;
367
368        let status = match listinvoices_response.invoices.first() {
369            Some(invoice_response) => cln_invoice_status_to_mint_state(invoice_response.status),
370            None => {
371                tracing::info!(
372                    "Check invoice called on unknown look up id: {}",
373                    payment_hash
374                );
375                return Err(Error::WrongClnResponse.into());
376            }
377        };
378
379        Ok(status)
380    }
381
382    async fn check_outgoing_payment(
383        &self,
384        payment_hash: &str,
385    ) -> Result<MakePaymentResponse, Self::Err> {
386        let mut cln_client = self.cln_client.lock().await;
387
388        let listpays_response = cln_client
389            .call_typed(&ListpaysRequest {
390                payment_hash: Some(payment_hash.parse().map_err(|_| Error::InvalidHash)?),
391                bolt11: None,
392                status: None,
393                start: None,
394                index: None,
395                limit: None,
396            })
397            .await
398            .map_err(Error::from)?;
399
400        match listpays_response.pays.first() {
401            Some(pays_response) => {
402                let status = cln_pays_status_to_mint_state(pays_response.status);
403
404                Ok(MakePaymentResponse {
405                    payment_lookup_id: pays_response.payment_hash.to_string(),
406                    payment_proof: pays_response.preimage.map(|p| hex::encode(p.to_vec())),
407                    status,
408                    total_spent: pays_response
409                        .amount_sent_msat
410                        .map_or(Amount::ZERO, |a| a.msat().into()),
411                    unit: CurrencyUnit::Msat,
412                })
413            }
414            None => Ok(MakePaymentResponse {
415                payment_lookup_id: payment_hash.to_string(),
416                payment_proof: None,
417                status: MeltQuoteState::Unknown,
418                total_spent: Amount::ZERO,
419                unit: CurrencyUnit::Msat,
420            }),
421        }
422    }
423}
424
425impl Cln {
426    /// Get last pay index for cln
427    async fn get_last_pay_index(&self) -> Result<Option<u64>, Error> {
428        let mut cln_client = self.cln_client.lock().await;
429        let listinvoices_response = cln_client
430            .call_typed(&ListinvoicesRequest {
431                index: None,
432                invstring: None,
433                label: None,
434                limit: None,
435                offer_id: None,
436                payment_hash: None,
437                start: None,
438            })
439            .await
440            .map_err(Error::from)?;
441
442        match listinvoices_response.invoices.last() {
443            Some(last_invoice) => Ok(last_invoice.pay_index),
444            None => Ok(None),
445        }
446    }
447}
448
449fn cln_invoice_status_to_mint_state(status: ListinvoicesInvoicesStatus) -> MintQuoteState {
450    match status {
451        ListinvoicesInvoicesStatus::UNPAID => MintQuoteState::Unpaid,
452        ListinvoicesInvoicesStatus::PAID => MintQuoteState::Paid,
453        ListinvoicesInvoicesStatus::EXPIRED => MintQuoteState::Unpaid,
454    }
455}
456
457fn cln_pays_status_to_mint_state(status: ListpaysPaysStatus) -> MeltQuoteState {
458    match status {
459        ListpaysPaysStatus::PENDING => MeltQuoteState::Pending,
460        ListpaysPaysStatus::COMPLETE => MeltQuoteState::Paid,
461        ListpaysPaysStatus::FAILED => MeltQuoteState::Failed,
462    }
463}
464
465async fn fetch_invoice_by_payment_hash(
466    cln_client: &mut cln_rpc::ClnRpc,
467    payment_hash: &str,
468) -> Result<Option<ListinvoicesInvoices>, Error> {
469    match cln_client
470        .call_typed(&ListinvoicesRequest {
471            payment_hash: Some(payment_hash.to_string()),
472            index: None,
473            invstring: None,
474            label: None,
475            limit: None,
476            offer_id: None,
477            start: None,
478        })
479        .await
480    {
481        Ok(invoice_response) => Ok(invoice_response.invoices.first().cloned()),
482        Err(e) => {
483            tracing::warn!("Error fetching invoice: {e}");
484            Err(Error::from(e))
485        }
486    }
487}