1#![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#[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 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 fn is_wait_invoice_active(&self) -> bool {
81 self.wait_invoice_is_active.load(Ordering::SeqCst)
82 }
83
84 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 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 is_active.store(false, Ordering::SeqCst);
115 return None;
117 }
118 result = cln_client.call_typed(&request) => {
119 match result {
120 Ok(invoice) => {
121
122 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 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 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}