lib.rs 17 KB

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