lib.rs 20 KB

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