lib.rs 19 KB

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